diff --git a/.gitignore b/.gitignore index 33fbe8a..75dbd82 100644 --- a/.gitignore +++ b/.gitignore @@ -418,4 +418,6 @@ expo-env.d.ts ios/ -android/ \ No newline at end of file +android/ + +*.env \ No newline at end of file diff --git a/.trunk/.gitignore b/.trunk/.gitignore new file mode 100644 index 0000000..15966d0 --- /dev/null +++ b/.trunk/.gitignore @@ -0,0 +1,9 @@ +*out +*logs +*actions +*notifications +*tools +plugins +user_trunk.yaml +user.yaml +tmp diff --git a/.trunk/configs/.hadolint.yaml b/.trunk/configs/.hadolint.yaml new file mode 100644 index 0000000..98bf0cd --- /dev/null +++ b/.trunk/configs/.hadolint.yaml @@ -0,0 +1,4 @@ +# Following source doesn't work in most setups +ignored: + - SC1090 + - SC1091 diff --git a/.trunk/configs/.markdownlint.yaml b/.trunk/configs/.markdownlint.yaml new file mode 100644 index 0000000..b40ee9d --- /dev/null +++ b/.trunk/configs/.markdownlint.yaml @@ -0,0 +1,2 @@ +# Prettier friendly markdownlint config (all formatting rules disabled) +extends: markdownlint/style/prettier diff --git a/.trunk/configs/.yamllint.yaml b/.trunk/configs/.yamllint.yaml new file mode 100644 index 0000000..184e251 --- /dev/null +++ b/.trunk/configs/.yamllint.yaml @@ -0,0 +1,7 @@ +rules: + quoted-strings: + required: only-when-needed + extra-allowed: ["{|}"] + key-duplicates: {} + octal-values: + forbid-implicit-octal: true diff --git a/.trunk/configs/svgo.config.js b/.trunk/configs/svgo.config.js new file mode 100644 index 0000000..b257d13 --- /dev/null +++ b/.trunk/configs/svgo.config.js @@ -0,0 +1,14 @@ +module.exports = { + plugins: [ + { + name: "preset-default", + params: { + overrides: { + removeViewBox: false, // https://github.com/svg/svgo/issues/1128 + sortAttrs: true, + removeOffCanvasPaths: true, + }, + }, + }, + ], +}; diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml new file mode 100644 index 0000000..24893fc --- /dev/null +++ b/.trunk/trunk.yaml @@ -0,0 +1,41 @@ +# This file controls the behavior of Trunk: https://docs.trunk.io/cli +# To learn more about the format of this file, see https://docs.trunk.io/reference/trunk-yaml +version: 0.1 +cli: + version: 1.22.2 +# Trunk provides extensibility via plugins. (https://docs.trunk.io/plugins) +plugins: + sources: + - id: trunk + ref: v1.6.0 + uri: https://github.com/trunk-io/plugins +# Many linters and tools depend on runtimes - configure them here. (https://docs.trunk.io/runtimes) +runtimes: + enabled: + - go@1.21.0 + - node@18.12.1 + - python@3.10.8 +# This is the section where you manage your linters. (https://docs.trunk.io/check/configuration) +lint: + enabled: + - checkov@3.2.186 + - dotenv-linter@3.3.0 + - git-diff-check + - gofmt@1.20.4 + - golangci-lint@1.59.1 + - hadolint@2.12.0 + - markdownlint@0.41.0 + - osv-scanner@1.8.2 + - oxipng@9.1.1 + - prettier@3.3.2 + - svgo@3.3.2 + - trivy@0.52.2 + - trufflehog@3.79.0 + - yamllint@1.35.1 +actions: + disabled: + - trunk-announce + - trunk-check-pre-push + - trunk-fmt-pre-commit + enabled: + - trunk-upgrade-available diff --git a/Backend/delivery_api/app/handlers/solicitations.go b/Backend/delivery_api/app/handlers/solicitations.go index c57e914..a6f7154 100644 --- a/Backend/delivery_api/app/handlers/solicitations.go +++ b/Backend/delivery_api/app/handlers/solicitations.go @@ -25,6 +25,7 @@ func CreateSolicitation(msg string, sendMessageToClient func(clientID int64, mes } collection := models.MongoDabase.Collection("solicitations") + log.Println(orderDTO) filter := bson.M{"orderid": orderDTO.OrderId} @@ -152,8 +153,8 @@ func GetApprovedSolicitations(c *fiber.Ctx) error { if err != nil { return err } - defer cur.Close(context.Background()) + defer cur.Close(context.Background()) for cur.Next(context.Background()) { var orderDTO dto.OrderDTO err := cur.Decode(&orderDTO) @@ -165,6 +166,7 @@ func GetApprovedSolicitations(c *fiber.Ctx) error { distance := calculateDistance(latitude, longitude, orderDTO.Establishment.Lat, orderDTO.Establishment.Long) // Se a distância for menor ou igual ao limite de distância, adiciona a solicitação à lista + if distance <= limitDist { approvedSolicitations = append(approvedSolicitations, orderDTO) } diff --git a/Backend/delivery_api/main.go b/Backend/delivery_api/main.go index 73be33d..785f292 100644 --- a/Backend/delivery_api/main.go +++ b/Backend/delivery_api/main.go @@ -161,6 +161,7 @@ func startQueueListener() { // Processar mensagens recebidas for msg := range msgs { bodyStr := string(msg.Body) + log.Printf("\n MENSAGEM ", bodyStr) handlers.CreateSolicitation(bodyStr, sendMessageToClient) } } diff --git a/Backend/docker-compose.yml b/Backend/docker-compose.yml index 097d7e1..48c3aff 100644 --- a/Backend/docker-compose.yml +++ b/Backend/docker-compose.yml @@ -69,6 +69,7 @@ services: - RABBIT_DELIVERY_QUEUE=MS_DELIVERY_QUEUE - RABBIT_ORDER_QUEUE=RABBIT_ORDER_QUEUE - URL_GET_ESTABLISHMENT_ID=http://api-gateway/api/auth/establishments/%d + - GOOGLE_MAPS_API_KEY= networks: - gateway-network ports: diff --git a/Backend/docs/delivery.postman_collection.json b/Backend/docs/delivery.postman_collection.json index f1c114c..41785df 100644 --- a/Backend/docs/delivery.postman_collection.json +++ b/Backend/docs/delivery.postman_collection.json @@ -1437,6 +1437,40 @@ }, "response": [] }, + { + "name": "Create Delivery - Avulso", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "{{AUTH_TOKEN}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"cart\": [\n {\n \"item\": {\n \"ID\": 0,\n \"Name\": \"Pizza Margherita\",\n \"Description\": \"Uma deliciosa pizza com molho de tomate, queijo mozzarella e folhas de manjericão fresco.\",\n \"Price\": 0,\n \"Image\": \"\",\n \"EstablishmentID\": 0,\n \"Categories\": null,\n \"Additional\": []\n },\n \"additionals\": [],\n \"quantity\": 1,\n \"id\": \"UTG5ksjsMUNBcqZ\"\n }\n ],\n \"distance\": 1.1365938003231386,\n \"location\": {\n \"cep\": \"01001-000\",\n \"logradouro\": \"Praça da Sé\",\n \"complemento\": null,\n \"bairro\": \"Sé\",\n \"localidade\": \"São Paulo\",\n \"uf\": \"SP\",\n \"ibge\": \"3550308\",\n \"gia\": \"1004\",\n \"ddd\": \"11\",\n \"siafi\": \"7107\",\n \"numero\": \"6\",\n \"coords\": {\n \"latitude\": -21.7725387727999,\n \"longitude\": -43.35824173879191\n }\n },\n \"paymentMethod\": {\n \"type\": \"credit\",\n \"icon\": \"credit-score\"\n },\n \"deliveryValue\": 3.12,\n \"user\": {\n \"phone\": \"(11) 98844-9999\",\n \"nome\": \"Ronan Silva\"\n },\n \"establishmentId\": 0,\n \"establishment\": {\n \"HorarioFuncionamento\": \"\",\n \"Id\": 1,\n \"Image\": \"https://cdn.pixabay.com/photo/2020/05/25/08/40/food-delivery-5217579_1280.png\",\n \"lat\": -21.7725387727999,\n \"long\": -43.35824173879191,\n \"max_distance_delivery\": 99999,\n \"name\": \"Luana Carla da Silva\",\n \"owner_id\": 1,\n \"primary_color\": \"#8B0000\",\n \"secondary_color\": \"#F0F8FF\",\n \"location_string\": \"Av. Atlântica, 1642 - Cavaleiros, Macaé - RJ, 27920-390, Brazil\"\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_GATEWAY}}/api/order/orders-single", + "host": [ + "{{API_GATEWAY}}" + ], + "path": [ + "api", + "order", + "orders-single" + ] + } + }, + "response": [] + }, { "name": "Update Status Order", "request": { @@ -1471,6 +1505,40 @@ } }, "response": [] + }, + { + "name": "Orders Single - Avulsa sem restaurante", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "{{AUTH_TOKEN}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"cart\": [\n {\n \"item\": {\n \"ID\": 0,\n \"Name\": \"Pizza Margherita\",\n \"Description\": \"Uma deliciosa pizza com molho de tomate, queijo mozzarella e folhas de manjericão fresco.\",\n \"Price\": 0,\n \"Image\": \"\",\n \"EstablishmentID\": 0,\n \"Categories\": null,\n \"Additional\": []\n },\n \"additionals\": [],\n \"quantity\": 1,\n \"id\": \"UTG5ksjsMUNBcqZ\"\n }\n ],\n \"distance\": 1.1365938003231386,\n \"location\": {\n \"cep\": \"01001-000\",\n \"logradouro\": \"Praça da Sé\",\n \"complemento\": null,\n \"bairro\": \"Sé\",\n \"localidade\": \"São Paulo\",\n \"uf\": \"SP\",\n \"ibge\": \"3550308\",\n \"gia\": \"1004\",\n \"ddd\": \"11\",\n \"siafi\": \"7107\",\n \"numero\": \"6\",\n \"coords\": {\n \"latitude\": -21.7725387727999,\n \"longitude\": -43.35824173879191\n }\n },\n \"paymentMethod\": {\n \"type\": \"credit\",\n \"icon\": \"credit-score\"\n },\n \"deliveryValue\": 20.00,\n \"user\": {\n \"phone\": \"(11) 98844-9999\",\n \"nome\": \"Ronan Silva\"\n },\n \"establishment\": {\n \"horarioFuncionamento\": \"\",\n \"image\": \"https://cdn.pixabay.com/photo/2020/05/25/08/40/food-delivery-5217579_1280.png\",\n \"lat\": -21.7725387727991,\n \"long\": -43.35824173879191,\n \"max_distance_delivery\": 1000,\n \"name\": \"Luana Carla da Silva\",\n \"owner_id\": 0,\n \"primary_color\": \"#8B0000\",\n \"secondary_color\": \"#F0F8FF\",\n \"location_string\": \"Av. Atlântica, 1642 - Cavaleiros, Macaé - RJ, 27920-390, Brazil\"\n }\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_GATEWAY}}/api/order/orders-single", + "host": [ + "{{API_GATEWAY}}" + ], + "path": [ + "api", + "order", + "orders-single" + ] + } + }, + "response": [] } ] } diff --git a/Backend/orders_api/.env b/Backend/orders_api/.env index 805ffd6..47f4eae 100644 --- a/Backend/orders_api/.env +++ b/Backend/orders_api/.env @@ -5,4 +5,5 @@ MONGO_DATABASE=orders_mongo_db RABBIT_CONNECTION=amqp://guest:guest@localhost:5672/ RABBIT_DELIVERY_QUEUE=MS_DELIVERY_QUEUE URL_GET_ESTABLISHMENT_ID=http://localhost/api/auth/establishments/%d -RABBIT_ORDER_QUEUE=RABBIT_ORDER_QUEUE \ No newline at end of file +RABBIT_ORDER_QUEUE=RABBIT_ORDER_QUEUE +GOOGLE_MAPS_API_KEY= \ No newline at end of file diff --git a/Backend/orders_api/app/handlers/localization.go b/Backend/orders_api/app/handlers/localization.go new file mode 100644 index 0000000..68c7ec6 --- /dev/null +++ b/Backend/orders_api/app/handlers/localization.go @@ -0,0 +1,99 @@ +package handlers + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + "os" +) + +type Coords struct { + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` +} + +type Location struct { + Cep string `json:"cep"` + Logradouro string `json:"logradouro"` + Complemento string `json:"complemento"` + Bairro string `json:"bairro"` + Localidade string `json:"localidade"` + Uf string `json:"uf"` + Ibge string `json:"ibge"` + Gia string `json:"gia"` + Ddd string `json:"ddd"` + Siafi string `json:"siafi"` + Numero string `json:"numero"` + Coords Coords `json:"coords"` +} + +func GetLocationDetails(address string) (*Location, error) { + apiKey := os.Getenv("GOOGLE_MAPS_API_KEY") + + apiURL := "https://maps.googleapis.com/maps/api/geocode/json" + reqURL := fmt.Sprintf("%s?address=%s&key=%s", apiURL, url.QueryEscape(address), apiKey) + + resp, err := http.Get(reqURL) + if err != nil { + return nil, fmt.Errorf("failed to make request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("received non-200 response code: %d", resp.StatusCode) + } + + var geocodeResponse struct { + Results []struct { + AddressComponents []struct { + LongName string `json:"long_name"` + ShortName string `json:"short_name"` + Types []string `json:"types"` + } `json:"address_components"` + Geometry struct { + Location struct { + Lat float64 `json:"lat"` + Lng float64 `json:"lng"` + } `json:"location"` + } `json:"geometry"` + FormattedAddress string `json:"formatted_address"` + } `json:"results"` + Status string `json:"status"` + } + + if err := json.NewDecoder(resp.Body).Decode(&geocodeResponse); err != nil { + return nil, fmt.Errorf("failed to decode response: %v", err) + } + + if geocodeResponse.Status != "OK" { + return nil, fmt.Errorf("geocoding API error: %s", geocodeResponse.Status) + } + + // Extrair as informações do primeiro resultado + result := geocodeResponse.Results[0] + location := &Location{} + + for _, component := range result.AddressComponents { + for _, t := range component.Types { + switch t { + case "postal_code": + location.Cep = component.LongName + case "route": + location.Logradouro = component.LongName + case "sublocality", "sublocality_level_1", "political": + location.Bairro = component.LongName + case "administrative_area_level_2": + location.Localidade = component.LongName + case "administrative_area_level_1": + location.Uf = component.ShortName + } + } + } + + // Coordenadas geográficas + location.Coords.Latitude = result.Geometry.Location.Lat + location.Coords.Longitude = result.Geometry.Location.Lng + + return location, nil +} diff --git a/Backend/orders_api/app/handlers/orders.go b/Backend/orders_api/app/handlers/orders.go index 0becb9f..d3f9171 100644 --- a/Backend/orders_api/app/handlers/orders.go +++ b/Backend/orders_api/app/handlers/orders.go @@ -2,6 +2,8 @@ package handlers import ( "context" + "crypto/rand" + "encoding/hex" "encoding/json" "fmt" "log" @@ -18,6 +20,61 @@ import ( "go.mongodb.org/mongo-driver/mongo/options" ) +func gerarHex(n int) (string, error) { + bytes := make([]byte, n) + if _, err := rand.Read(bytes); err != nil { + return "", err + } + return hex.EncodeToString(bytes), nil +} + +func CreateDeliveryOrder(c *fiber.Ctx, sendMessageToClient func(clientID int64, message []byte) error) error { + var request dto.RequestPayload + + if err := c.BodyParser(&request); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Erro ao fazer parsing do corpo da requisição", + }) + } + + /// Diferentemente do createOrder padrão que precisa da aceitação do restaunrate. + /// Na entrega avulsa você consegue solicitar só a parte de entrega, se restaurantes. + request.Status = "DONE" + + collection := models.MongoDabase.Collection("orders") + + insertResult, err := collection.InsertOne(context.Background(), request) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Erro ao inserir a ordem no banco de dados", + }) + } + + if oid, ok := insertResult.InsertedID.(primitive.ObjectID); ok { + request.OrderId = oid.Hex() + } + + jsonData, _ := json.Marshal(request) + + if err := sendMessageToClient(0, jsonData); err != nil { + return err + } + + requestBytes, _ := json.Marshal(&request) + + err = PublishMessage(requestBytes) + if err != nil { + log.Println(err) + } + + return c.JSON(fiber.Map{ + "message": "Ordem criada com sucesso", + "orderId": insertResult.InsertedID, + }) + +} + +// Esse método não publica na fila do delivery, pois ele precisa de aprovação do estabelecimento. func CreateOrder(c *fiber.Ctx, sendMessageToClient func(clientID int64, message []byte) error) error { var request dto.RequestPayload @@ -104,13 +161,13 @@ func UpdateOrderStatus(c *fiber.Ctx, sendMessageToClient func(clientID int64, me } var order dto.RequestPayload - collection.FindOne(context.Background(), filter).Decode(&order) + _ = collection.FindOne(context.Background(), filter).Decode(&order) if requestBody.Status != "REQUEST_APPROVE" { order.OrderId = orderID.Hex() order.Status = requestBody.Status orderBytes, err := json.Marshal(&order) if err == nil { - PublishMessage(orderBytes) + _ = PublishMessage(orderBytes) } } @@ -207,7 +264,7 @@ func ListOrdersByEstablishmentIDAndPhone(c *fiber.Ctx) error { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "ID do estabelecimento inválido"}) } - phoneNumber, err := url.QueryUnescape(phoneNumberEncoded) + phoneNumber, _ := url.QueryUnescape(phoneNumberEncoded) filter := bson.M{ "establishmentid": establishmentIDInt, "user.phone": phoneNumber, diff --git a/Backend/orders_api/app/handlers/rabbit.go b/Backend/orders_api/app/handlers/rabbit.go index a36f076..c63bf53 100644 --- a/Backend/orders_api/app/handlers/rabbit.go +++ b/Backend/orders_api/app/handlers/rabbit.go @@ -120,7 +120,7 @@ func PublishMessage(body []byte) error { return err } - // log.Printf(" [x] Sent %s", body) + log.Printf(" [x] Sent %s", body) return nil } diff --git a/Backend/orders_api/app/models/Order.go b/Backend/orders_api/app/models/Order.go index 75f9a1a..6c24ca4 100644 --- a/Backend/orders_api/app/models/Order.go +++ b/Backend/orders_api/app/models/Order.go @@ -5,5 +5,5 @@ type Order struct { UserID uint `gorm:"foreignKey:IDUsuario"` EstablishmentID uint OrderDate string - Status string // Pending, In Progress, Delivered, etc. + Status string } diff --git a/Backend/orders_api/app/routes/routes.go b/Backend/orders_api/app/routes/routes.go index b8d8d63..273f501 100644 --- a/Backend/orders_api/app/routes/routes.go +++ b/Backend/orders_api/app/routes/routes.go @@ -1,6 +1,8 @@ package routes import ( + "fmt" + "github.com/carloshomar/vercardapio/app/handlers" "github.com/gofiber/fiber/v2" ) @@ -35,11 +37,14 @@ func SetupRoutes(app *fiber.App, sendMessageToClient func(clientID int64, messag app.Post("/delivery/calculate-delivery-value", handlers.CalculateDeliveryValue) app.Get("/delivery/value/:establishmentId", handlers.GetDeliveryByEstablishmentID) - app.Post("/orders", func(c *fiber.Ctx) error { return handlers.CreateOrder(c, sendMessageToClient) }) + app.Post("/orders-single", func(c *fiber.Ctx) error { + return handlers.CreateDeliveryOrder(c, sendMessageToClient) + }) + app.Put("/orders/status", func(c *fiber.Ctx) error { return handlers.UpdateOrderStatus(c, sendMessageToClient) }) @@ -48,4 +53,22 @@ func SetupRoutes(app *fiber.App, sendMessageToClient func(clientID int64, messag app.Get("/orders/:establishmentId", handlers.ListOrdersByEstablishmentID) app.Get("/orders/:establishmentId/:phoneNumber", handlers.ListOrdersByEstablishmentIDAndPhone) + app.Get("/localization/get-location", func(c *fiber.Ctx) error { + address := c.Query("address") + if address == "" { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Address query parameter is required", + }) + } + + location, err := handlers.GetLocationDetails(address) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": fmt.Sprintf("Failed to get location details: %v", err), + }) + } + + return c.JSON(location) + }) + } diff --git a/Frontend/AppComida/services/api.tsx b/Frontend/AppComida/services/api.tsx index ba49eac..81e9e9c 100644 --- a/Frontend/AppComida/services/api.tsx +++ b/Frontend/AppComida/services/api.tsx @@ -4,7 +4,7 @@ import axios from "axios"; import AsyncStorage from "@react-native-async-storage/async-storage"; -const baseURL = process.env.API_BASE_URL || "http://127.0.0.1"; +const baseURL = process.env.API_BASE_URL ?? "http://192.168.68.103"; const api = axios.create({ baseURL: baseURL, diff --git a/Frontend/AppEntrega/app.json b/Frontend/AppEntrega/app.json index 1b01755..be53a34 100644 --- a/Frontend/AppEntrega/app.json +++ b/Frontend/AppEntrega/app.json @@ -17,7 +17,11 @@ "assetBundlePatterns": ["**/*"], "ios": { "supportsTablet": true, - "bundleIdentifier": "com.coopdelivery.delivery" + "bundleIdentifier": "com.coopdelivery.delivery", + "infoPlist": { + "NSLocationWhenInUseUsageDescription": "This app needs access to your location to provide personalized recommendations.", + "NSLocationAlwaysUsageDescription": "This app needs access to your location at all times to track your fitness activities." + } }, "android": { "package": "com.coopdelivery.delivery", diff --git a/Frontend/AppEntrega/app/(tabs)/index.tsx b/Frontend/AppEntrega/app/(tabs)/index.tsx index b63c1b1..128005a 100755 --- a/Frontend/AppEntrega/app/(tabs)/index.tsx +++ b/Frontend/AppEntrega/app/(tabs)/index.tsx @@ -16,6 +16,6 @@ function index() { if (isFocused) nav.setOptions({ title: Strings.inicio }); }, [isFocused]); - return inWork.status ? : ; + return inWork?.status ? : ; } export default index; diff --git a/Frontend/AppEntrega/app/(tabs)/two.tsx b/Frontend/AppEntrega/app/(tabs)/two.tsx index d9c4294..c41c06f 100755 --- a/Frontend/AppEntrega/app/(tabs)/two.tsx +++ b/Frontend/AppEntrega/app/(tabs)/two.tsx @@ -1,6 +1,5 @@ -import { ScrollView, StyleSheet, Text, View } from "react-native"; +import { StyleSheet, Text, View } from "react-native"; -import EditScreenInfo from "@/components/EditScreenInfo"; import SolicitationList from "@/componentes/SolicitationList"; import Colors from "@/constants/Colors"; import { useEffect, useState } from "react"; @@ -34,7 +33,7 @@ export default function TabTwoScreen() { return ( - {orders.length === 0 && !load ? ( + {orders?.length === 0 && !load ? ( {Texts.notfound_extract} diff --git a/Frontend/AppEntrega/app/confirm_generical.tsx b/Frontend/AppEntrega/app/confirm_generical.tsx index 029df57..1c57ab6 100755 --- a/Frontend/AppEntrega/app/confirm_generical.tsx +++ b/Frontend/AppEntrega/app/confirm_generical.tsx @@ -31,6 +31,8 @@ export default function ConfirmGenerical() { } function verify() { + console.log("Codigos: ", hasCode, code); + if (hasCode === code) { handlerConfirm(); } else { diff --git a/Frontend/AppEntrega/app/delivery_mode.tsx b/Frontend/AppEntrega/app/delivery_mode.tsx index 5dea83a..3d67e76 100755 --- a/Frontend/AppEntrega/app/delivery_mode.tsx +++ b/Frontend/AppEntrega/app/delivery_mode.tsx @@ -79,6 +79,14 @@ export default function DeliveryMode({ showIcon }: any) { break; } + // O cenário desse if só acontece para casos de entrega desvinculadas do restaurante (pacotes), onde establishment vem zerado + if ( + deliveryman.status === "AWAIT_COLECT" && + order?.establishment?.Id === 0 + ) { + code = false; + } + nav.navigate("confirm_generical", { onConfirm: awaitCollect, hasCode: code, diff --git a/Frontend/AppEntrega/app/pages/home.tsx b/Frontend/AppEntrega/app/pages/home.tsx index 556b3f2..8cadd23 100755 --- a/Frontend/AppEntrega/app/pages/home.tsx +++ b/Frontend/AppEntrega/app/pages/home.tsx @@ -25,6 +25,7 @@ import Strings from "@/constants/Strings"; import { useIsFocused } from "@react-navigation/native"; import { useAuthApi } from "@/contexts/AuthContext"; import Config from "@/constants/Config"; +import deliveryModel from "@/services/delivery.model"; export default function Home() { const { @@ -53,12 +54,13 @@ export default function Home() { latitude: 0, longitude: 0, }; - mapViewRef.current?.animateToRegion({ - latitude, - longitude, - latitudeDelta: 0.05, - longitudeDelta: 0.003, - }); + if (mapViewRef.current) + mapViewRef.current?.animateToRegion({ + latitude, + longitude, + latitudeDelta: 0.05, + longitudeDelta: 0.003, + }); } }; @@ -103,37 +105,17 @@ export default function Home() { async function disponify(focused = true, loader = true) { if (loader) setLoading(true); - try { - const { data } = await api.get( - `/api/delivery/solicitation-orders?latitude=${mylocation.coords.latitude}&longitude=${mylocation.coords.longitude}&limitDistance=${Strings.distance_delivery_distance}` - ); - - const marks = data.map((mak: any) => { - return { - id: mak.establishmentId, - name: mak.establishment.name, - location_string: mak.establishment.location_string, - coordinates: { - latitude: mak.establishment?.lat ?? 0, - longitude: mak.establishment?.long ?? 0, - }, - isEstablishment: true, - valueDelivery: mak.deliveryValue, - distance: mak.distance, - order_id: mak.order_id, - }; - }); - const final = [...marks, helper.getMarkerUser(mylocation)]; + const marks = await deliveryModel.getLocation(mylocation); + console.log(marks); + const final = [...(marks ?? []), helper.getMarkerUser(mylocation)]; + setMarkers(final); - setMarkers(final); - if (focused) { - centerMapOnUser(); - } - } catch (e) { - setMarkers([helper.getMarkerUser(mylocation)]); + if (focused) { + centerMapOnUser(); } - if (loader) setLoading(false); + + setLoading(false); } async function clearMap() { @@ -195,7 +177,7 @@ export default function Home() { { const nav = useNavigation(); const groupOrdersByDate = () => { const groupedOrders = {} as any; - orders.forEach((order: any) => { + orders?.forEach((order: any) => { const day = helper.formatDateNoHour(order.operationDate); if (!groupedOrders[day]) { groupedOrders[day] = []; diff --git a/Frontend/AppEntrega/services/api.tsx b/Frontend/AppEntrega/services/api.tsx index b34109d..8c2393c 100755 --- a/Frontend/AppEntrega/services/api.tsx +++ b/Frontend/AppEntrega/services/api.tsx @@ -3,7 +3,7 @@ import axios from "axios"; import AsyncStorage from "@react-native-async-storage/async-storage"; -const baseURL = process.env.API_BASE_URL || "http://127.0.0.1"; +const baseURL = process.env.API_BASE_URL ?? "http://192.168.68.103"; const api = axios.create({ baseURL: baseURL, diff --git a/Frontend/AppEntrega/services/delivery.model.ts b/Frontend/AppEntrega/services/delivery.model.ts new file mode 100644 index 0000000..5f702af --- /dev/null +++ b/Frontend/AppEntrega/services/delivery.model.ts @@ -0,0 +1,37 @@ +import Strings from "@/constants/Strings"; +import api from "./api"; + +async function getLocation({ coords }: any) { + if (!coords || !coords?.latitude || !coords?.longitude) { + return []; + } + + try { + const { data } = await api.get( + `/api/delivery/solicitation-orders?latitude=${coords.latitude}&longitude=${coords.longitude}&limitDistance=${Strings.distance_delivery_distance}` + ); + + const marks = data?.map((mak: any) => { + return { + id: mak.establishmentId, + name: mak.establishment.name, + location_string: mak.establishment.location_string, + coordinates: { + latitude: mak.establishment?.lat ?? 0, + longitude: mak.establishment?.long ?? 0, + }, + isEstablishment: true, + valueDelivery: mak.deliveryValue, + distance: mak.distance, + order_id: mak.order_id, + }; + }); + + return marks; + } catch (e) { + console.log(e); + return []; + } +} + +export default { getLocation }; diff --git a/Frontend/WebRestaurant/src/Routes.js b/Frontend/WebRestaurant/src/Routes.js index 10be197..769a29d 100644 --- a/Frontend/WebRestaurant/src/Routes.js +++ b/Frontend/WebRestaurant/src/Routes.js @@ -6,6 +6,7 @@ import { createBrowserRouter, RouterProvider } from "react-router-dom"; import Cardapio from "./pages/cardapio/products/Cardapio"; import Perfil from "./pages/perfil"; import Taxes from "./pages/perfil/taxes"; +import Delivery from "./pages/delivery"; const router = createBrowserRouter([ { @@ -24,6 +25,10 @@ const router = createBrowserRouter([ path: "/taxas", element: , }, + { + path: "/delivery-avulso", + element: , + }, ]); export default function PrivateRoute() { diff --git a/Frontend/WebRestaurant/src/constants/Strings.js b/Frontend/WebRestaurant/src/constants/Strings.js index 6311055..3f21ca5 100644 --- a/Frontend/WebRestaurant/src/constants/Strings.js +++ b/Frontend/WebRestaurant/src/constants/Strings.js @@ -1,3 +1,5 @@ +import Texts from "./Texts"; + const token_jwt = "JWT_TOKEN"; const initial_order = (item) => { return { @@ -12,8 +14,66 @@ const initial_order = (item) => { }; const id_default = -9999999999; + +const orderAvulsa = { + cart: [], + distance: "", + location: { + cep: "", + logradouro: "", + complemento: "", + bairro: "", + localidade: "", + uf: "", + ibge: "", + gia: "", + ddd: "", + siafi: "", + numero: "", + coords: { + latitude: "", + longitude: "", + }, + }, + paymentMethod: { + type: "", + icon: "", + }, + deliveryValue: 10, + user: { + phone: "", + nome: "", + }, + establishment: { + horarioFuncionamento: "", + image: "", + lat: "", + long: "", + max_distance_delivery: 999999, + name: "", + owner_id: 0, + primary_color: "", + secondary_color: "", + location_string: "", + }, +}; + +const itemOrder = { + ID: "", + Name: Texts.entregaAvulsa, + Description: "", + Price: 0, + Image: "", + EstablishmentID: 0, + Categories: null, + Additional: [], + quantity: 1, + id: "", +}; export default { token_jwt, initial_order, id_default, + orderAvulsa, + itemOrder, }; diff --git a/Frontend/WebRestaurant/src/constants/Texts.js b/Frontend/WebRestaurant/src/constants/Texts.js index 8e5b696..fd1edf4 100644 --- a/Frontend/WebRestaurant/src/constants/Texts.js +++ b/Frontend/WebRestaurant/src/constants/Texts.js @@ -14,6 +14,7 @@ export default { nome: "Nome", status: "Status", preco: "Preço", + entregaAvulsta: "Entrega Avulsa", acoes: "Ações", remover_produto: "Remover Produto", search_placeholer: "Buscar...", diff --git a/Frontend/WebRestaurant/src/pages/delivery/index.js b/Frontend/WebRestaurant/src/pages/delivery/index.js new file mode 100644 index 0000000..a2b697f --- /dev/null +++ b/Frontend/WebRestaurant/src/pages/delivery/index.js @@ -0,0 +1,171 @@ +import React, { useState } from "react"; +import MenuLayout from "../../components/Menu"; +import Strings from "../../constants/Strings"; +import Step1 from "./Step1"; +import Step2 from "./Step2"; +import Step3 from "./Step3"; +import Step4 from "./Step4"; +import Step5 from "./Step5"; +import localizationModel from "../../services/localization.model"; + +function Delivery() { + const [formData, setFormData] = useState(Strings.orderAvulsa); + const [item, setItem] = useState([ + { + ID: "", + Name: "", + Description: "", + Price: "", + Image: "", + EstablishmentID: "", + Categories: "", + Additional: [], + quantity: "", + id: "", + }, + ]); + + const [step, setStep] = useState(1); + + const handleChange = (e, section, key) => { + const value = e.target?.value ?? e; + + if (key === "busca" && value.length > 2) { + consultaCep(value); + } + + setFormData((prevState) => ({ + ...prevState, + [section]: { + ...prevState[section], + [key]: value, + }, + })); + }; + + const handleItemChange = (e, key) => { + const value = e.target.value; + setItem((prevState) => ({ + ...prevState, + [key]: value, + })); + }; + + const consultaCep = async (busca) => { + try { + const data = await localizationModel.getLocalization(busca); + if (!data) { + return; + } + handleChange(data.bairro, "location", "bairro"); + handleChange(data.cep, "location", "cep"); + handleChange(data.logradouro, "location", "logradouro"); + handleChange(data.uf, "location", "uf"); + handleChange(data.localidade, "location", "localidade"); + + if (data.numero) handleChange(data.numero, "location", "numero"); + if (data.complemento) + handleChange(data.complemento, "location", "complemento"); + + setFormData((prevState) => ({ + ...prevState, + location: { + ...prevState.location, + coords: { + latitude: parseFloat(data?.coords?.latitude ?? 0), + longitude: parseFloat(data?.coords?.longitude ?? 0), + }, + }, + })); + } catch (e) { + console.log(e); + } + }; + + const handleSubmit = (e) => { + e.preventDefault(); + console.log({ + ...formData, + cart: [item], + }); + // Aqui você pode enviar formData para o seu servidor ou processar como necessário + }; + + const nextStep = () => { + setStep((prevStep) => prevStep + 1); + }; + + const prevStep = () => { + setStep((prevStep) => prevStep - 1); + }; + + const renderStep = () => { + switch (step) { + default: + case 1: + return ; + case 2: + return ( + + ); + case 3: + return ; + case 4: + return ; + case 5: + return ; + } + }; + + return ( + +
+

+ Delivery Avulso +

+
+
+
+ {renderStep()} +
+ {step < 5 && ( + + )} + + {step === 5 && ( + + )} + + {step > 1 && ( + + )} +
+
+
+
+ ); +} + +export default Delivery; diff --git a/Frontend/WebRestaurant/src/pages/delivery/step1.js b/Frontend/WebRestaurant/src/pages/delivery/step1.js new file mode 100644 index 0000000..493a105 --- /dev/null +++ b/Frontend/WebRestaurant/src/pages/delivery/step1.js @@ -0,0 +1,39 @@ +import React from "react"; + +function Step1({ formData, handleChange }) { + return ( +
+

Informações do Usuário

+
+
+ + handleChange(e, "user", "nome")} + className="border p-2 w-full" + /> +
+
+ + handleChange(e, "user", "phone")} + className="border p-2 w-full" + /> +
+
+
+ ); +} + +export default Step1; diff --git a/Frontend/WebRestaurant/src/pages/delivery/step2.js b/Frontend/WebRestaurant/src/pages/delivery/step2.js new file mode 100644 index 0000000..63785aa --- /dev/null +++ b/Frontend/WebRestaurant/src/pages/delivery/step2.js @@ -0,0 +1,50 @@ +import React from "react"; + +function Step2({ item, formData, handleChange, handleItemChange }) { + return ( +
+

Informações do Pacote

+
+
+ + handleItemChange(e, "Description")} + className="border p-2 w-full" + /> +
+
+ + +
+
+
+ ); +} + +export default Step2; diff --git a/Frontend/WebRestaurant/src/pages/delivery/step3.js b/Frontend/WebRestaurant/src/pages/delivery/step3.js new file mode 100644 index 0000000..977fcf1 --- /dev/null +++ b/Frontend/WebRestaurant/src/pages/delivery/step3.js @@ -0,0 +1,86 @@ +import React from "react"; + +function Step3({ formData, handleChange }) { + return ( +
+

Local de Coleta

+
+
+ + handleChange(e, "location", "busca")} + className="border p-2 w-full" + /> +
+
+ + {formData.location.cep !== "" && ( +
+
+ + handleChange(e, "location", "numero")} + className="border p-2 w-full" + /> +
+
+ + handleChange(e, "location", "complemento")} + className="border p-2 w-full" + /> +
+
+ )} + + {formData.location.cep !== "" && ( +
+ + CEP: {formData.location.cep} + + + Logradouro: {formData.location.logradouro} + + + Bairro: {formData.location.bairro} + + + Localidade: {formData.location.localidade} + + + UF: {formData.location.uf} + + + Número: {formData.location.numero} + + + Complemento: {formData.location.complemento} + +
+ )} +
+ ); +} + +export default Step3; diff --git a/Frontend/WebRestaurant/src/pages/delivery/step4.js b/Frontend/WebRestaurant/src/pages/delivery/step4.js new file mode 100644 index 0000000..1bed5be --- /dev/null +++ b/Frontend/WebRestaurant/src/pages/delivery/step4.js @@ -0,0 +1,148 @@ +import React, { useEffect, useState } from "react"; +import localizationModel from "../../services/localization.model"; + +function Step4({ formData, handleChange }) { + const [endereco, setEndereco] = useState(""); + const [address, setAddress] = useState({ cep: null }); + + async function preencherEndereco() { + if (endereco.length <= 3) { + return; + } + + try { + const response = await localizationModel.getLocalization(endereco); + + if (response) { + handleChange( + parseFloat(response?.coords?.latitude ?? 0), + "establishment", + "lat" + ); + handleChange( + parseFloat(response?.coords?.longitude ?? 0), + "establishment", + "long" + ); + + const addressText = `${response.logradouro}, ${response.numero}, ${response.complemento}, ${response.bairro}, ${response.localidade} - ${response.uf}, CEP: ${response.cep}.`; + handleChange( + { + target: { + value: addressText, + }, + }, + "establishment", + "location_string" + ); + setAddress(response); + } + } catch (error) { + console.error("Erro ao buscar o endereço:", error); + } + } + + useEffect(() => { + preencherEndereco(); + }, [endereco]); + + return ( +
+

Local de Entrega

+
+
+ + handleChange(e, "establishment", "name")} + className="border p-2 w-full" + /> +
+
+ + setEndereco(e.target.value)} + className="border p-2 w-full" + /> +
+
+ + {address?.cep !== null && ( +
+
+ + + setAddress({ ...address, numero: e.target.value }) + } + className="border p-2 w-full" + /> +
+
+ + + setAddress({ ...address, complemento: e.target.value }) + } + className="border p-2 w-full" + /> +
+
+ )} + + {address?.cep !== null && ( +
+ + CEP: {address?.cep} + + + Logradouro: {address?.logradouro} + + + Bairro: {address?.bairro} + + + Localidade: {address?.localidade} + + + UF: {address?.uf} + + + Número: {address?.numero} + + + Complemento: {address?.complemento} + +
+ )} +
+ ); +} + +export default Step4; diff --git a/Frontend/WebRestaurant/src/pages/delivery/step5.js b/Frontend/WebRestaurant/src/pages/delivery/step5.js new file mode 100644 index 0000000..ad3caa0 --- /dev/null +++ b/Frontend/WebRestaurant/src/pages/delivery/step5.js @@ -0,0 +1,97 @@ +import React from "react"; + +function Step5({ formData }) { + const { + cart = [], + distance, + location, + paymentMethod, + deliveryValue, + user, + establishment, + } = formData; + + return ( +
+

Resumo do Pedido

+ +
+

Entrega:

+ {cart.length > 0 ? ( +
+

+ Nome: {cart[0].Name} +

+

+ Descrição: {cart[0].Description} +

+

+ ID do Estabelecimento: {cart[0].EstablishmentID} +

+

+ ID: {cart[0].id} +

+
+ ) : ( +

Nenhum item no carrinho.

+ )} +
+ +
+

+ Localização da Retirada +

+
+

+ {`${location.logradouro}, ${location.numero}, ${location.complemento}, ${location.bairro}, ${location.localidade} - ${location.uf}, CEP: ${location.cep}.`} +

+
+
+ +
+

+ Localização da Entrega +

+
+

{establishment.location_string}

+
+
+ +
+

+ Método de Pagamento +

+
+

+ Tipo: {paymentMethod.type} +

+
+
+ +
+

Contato

+
+

+ Telefone: {user.phone} +

+

+ Nome: {user.nome} +

+
+
+ +
+

+ Valor da Entrega +

+
+

+ Valor: {deliveryValue} +

+
+
+
+ ); +} + +export default Step5; diff --git a/Frontend/WebRestaurant/src/services/localization.model.js b/Frontend/WebRestaurant/src/services/localization.model.js new file mode 100644 index 0000000..73e6f2c --- /dev/null +++ b/Frontend/WebRestaurant/src/services/localization.model.js @@ -0,0 +1,37 @@ +import Strings from "../constants/Strings"; +import api from "./api"; + +async function getLocalization(id) { + return { + cep: "12345-678", + logradouro: "Rua das Flores", + complemento: "Apto 101", + bairro: "Jardim Primavera", + localidade: "São Paulo", + uf: "SP", + ibge: "3550308", + gia: "1234", + ddd: "11", + siafi: "7107", + numero: "456", + coords: { + latitude: "-23.550520", + longitude: "-46.633308", + }, + }; + + try { + const { data } = await api.get( + `/api/order/localization/get-location?address=${id}` + ); + + return data; + } catch (e) { + console.log(e); + return []; + } +} + +export default { + getLocalization, +}; diff --git a/README.md b/README.md index 86a7bee..e6e1834 100644 --- a/README.md +++ b/README.md @@ -235,6 +235,7 @@ _Utilizei o algoritimo de Haversine para evitar o uso de APIs de mapas, por sere - É permitida somente uma entrega por vez, por entregador. **_(Existe a possibilidade de adição de uma fila de pedidos para entrega no AppEntrega por se tratar de um array, pretendo adicionar como feature futura)_** - No endpoint Delivery/Orders, o entregador envia sua localização e recebe os pedidos ao redor, todos os entregadores enviam um "sinal de vida" com sua localização. **_(Pretendo utilizar esse endpoint para rastreio das localizações percorridas pelo entregador, inclusive seu caminho percorrido para mapear a locomoção, cálculos de gastos calóricos e etc.)_** - Caso a entrega seja cancelada/removida, ou o entregador seja removido da entrega (**somente via banco, no mongoDB**), no próximo "sinal de vida" essa condição será refletida no app do mesmo e ele volta a ficar disponível para novas entregas. Sendo assim, existe a possiblidade da feature de remoção/sobreposição de um entregador em uma respectiva entrega, más não pretendo desenvolver. +- Existe a possibilidade de fazer entregas que não estejam vinculadas ao restaurante, exemplo: transporte/entrega de pacotes e documentos, nesse caso vai haver somente a necessidade de código no momento da entrega, diferente de quando vinculado ao restaurante onde o código do restaurante também é necessário. #### Restaurante: @@ -245,7 +246,7 @@ _Utilizei o algoritimo de Haversine para evitar o uso de APIs de mapas, por sere - A visualização do cliente é composta primeiro das categorias exibindo produtos agrupados, na página do restaurante, no AppComida e após as categorias vem uma listagem geral. - Os adicionais podem ou não possuir algum valor, que em caso de existência é refletido no valor do pedido. -#### Etapas de Entrega: +#### Etapas de Entrega com restaurante: - O restaurante faz o cadastramento de todos os seus produtos e "abre o estabelecimento" na aplicação WEBRestaurante. - O cliente realiza o pedido e indica a forma de pagamento, que pode ser feita na entrega. Atualmente, não temos integração com APIs de pagamento, mas isso pode ser implementado em qualquer linguagem e facilmente devido à arquitetura. @@ -256,10 +257,19 @@ _Utilizei o algoritimo de Haversine para evitar o uso de APIs de mapas, por sere - Ao receber o pedido, o entregador pode se locomover ao encontro do cliente e, ao entregar o pedido, solicitar o respectivo código de entrega (código de quatro dígitos disponível na área de pedidos no APP do Cliente). - _**Disclamer**: no código atual da **main**, adicionei o Código do Cliente (em vermelho) no card do pedido, isso é para facilitar os teste, em caso de publicação o ideal é remover o **Código do Cliente** da Visualização do Restaurante (Quadro Kanban), más pode variar de acordo com a regra de negócio._ - Após a entrega, o pedido sai do Painel Principal (Quadro Kanban) do restaurante, e a entrega é salva na tela de **Extrato do entregador**. +- O resumo da entrega fica disponivel para o entregador na aba Extrato. **_O fluxo acima de Etapas de Entrega pode ser visualizado no primeiro video._** +#### Etapas de Entrega sem restaurante: + +- Por meio do WebRestaurante o cliente solicita uma entrega avulsa. +- A solicitação segue o mesmo fluxo com a parte do entregador, onde a mesma aparece no AppEntregas para os entregadores ao redor. +- O entregador se locomove até o local de coleta e pega o pacote com o solicitante. +- O entregador se locomove até o local de entrega e solicita o Código ao destinatário, a entrega só pode ser finalizada com o Código. +- O resumo da entrega fica disponivel para o entregador na aba Extrato. + #### Publicação: