diff --git a/backend/gestion/db/db_commands.go b/backend/gestion/db/db_commands.go index cdbf4577..4d6728fa 100644 --- a/backend/gestion/db/db_commands.go +++ b/backend/gestion/db/db_commands.go @@ -374,13 +374,17 @@ func (d *Database) GetCommandByID(id int) (map[string]any, error) { ReferralUsed float64 `gorm:"column:referral_used"` ClientOrderNumber int `gorm:"column:client_order_number"` CancelReason string `gorm:"column:cancel_reason"` + DestLatitude float64 `gorm:"column:dest_latitude"` + DestLongitude float64 `gorm:"column:dest_longitude"` } if err := d.GDB.Table("commandes c"). Select(`c.id, c.username, c.status, c.adresse, c.total_prix, c.livreur_assign, c.created_at, c.updated_at, c.proposed_address, c.address_proposal_status, c.referral_used, c.client_order_id AS client_order_number, - COALESCE(c.cancel_reason, '') AS cancel_reason`). + COALESCE(c.cancel_reason, '') AS cancel_reason, + COALESCE(c.dest_latitude, 0) AS dest_latitude, + COALESCE(c.dest_longitude, 0) AS dest_longitude`). Where("c.id = ?", id). First(&row).Error; err != nil { return nil, fmt.Errorf("erreur lors de la récupération de la commande: %w", err) @@ -401,6 +405,8 @@ func (d *Database) GetCommandByID(id int) (map[string]any, error) { "referral_used": row.ReferralUsed, "client_order_number": row.ClientOrderNumber, "cancel_reason": row.CancelReason, + "dest_latitude": row.DestLatitude, + "dest_longitude": row.DestLongitude, } if row.LivreurAssign != nil { @@ -418,6 +424,31 @@ func (d *Database) GetCommandByID(id int) (map[string]any, error) { return command, nil } +// GetLastDeliveryCoords retourne les coordonnées GPS de la dernière livraison terminée d'un livreur. +// Utilisé comme fallback quand le GPS temps réel est indisponible. +func (d *Database) GetLastDeliveryCoords(livreurUsername string) (float64, float64, error) { + var result struct { + DestLatitude float64 `gorm:"column:dest_latitude"` + DestLongitude float64 `gorm:"column:dest_longitude"` + } + + if err := d.GDB.Table("commandes"). + Select("dest_latitude, dest_longitude"). + Where("livreur_assign = ? AND status IN (?, ?, ?) AND dest_latitude IS NOT NULL AND dest_latitude != 0 AND dest_longitude IS NOT NULL AND dest_longitude != 0", + livreurUsername, "livre", "delivered", "approved"). + Order("updated_at DESC"). + Limit(1). + Scan(&result).Error; err != nil { + return 0, 0, fmt.Errorf("aucune livraison précédente pour %s: %w", livreurUsername, err) + } + + if result.DestLatitude == 0 || result.DestLongitude == 0 { + return 0, 0, fmt.Errorf("coordonnées introuvables pour dernière livraison de %s", livreurUsername) + } + + return result.DestLatitude, result.DestLongitude, nil +} + // GetClientOrderID retourne le client_order_id (numéro perso du client) pour un commandID global. // Retourne commandID en fallback si introuvable. func (d *Database) GetClientOrderID(commandID int) int { diff --git a/backend/gestion/handlers/commands.go b/backend/gestion/handlers/commands.go index 50c7b725..5287fb75 100644 --- a/backend/gestion/handlers/commands.go +++ b/backend/gestion/handlers/commands.go @@ -3,8 +3,10 @@ package handlers import ( "bytes" "encoding/csv" + "encoding/json" "fmt" "gestion/db" + "gestion/services" "gestion/utils" "log" "net/http" @@ -149,6 +151,7 @@ func UpdateCommandAddress(c *gin.Context) { }) } +// ProposeAddressChange propose une nouvelle adresse au client pour validation func ProposeAddressChange(c *gin.Context) { database := c.MustGet("database").(*db.Database) @@ -629,6 +632,7 @@ func GetAvailableDeliveryPersons(c *gin.Context) { // Cabine: POST /api/v1/cabine/commands/:id/assign (body: {"livreur_username": "..."}) func AssignDeliveryPerson(c *gin.Context) { database := c.MustGet("database").(*db.Database) + geoService := c.MustGet("geoService").(*services.GeoService) // ✅ SÉCURITÉ: Admin ou Cabine role := c.GetString("role") @@ -678,6 +682,39 @@ func AssignDeliveryPerson(c *gin.Context) { fmt.Sprintf("Livreur '%s' assigné manuellement par %s", livreurUsername, staffUsername), staffUsername.(string)) + // Géocodage async : stocker les coords si absentes + go func() { + cmd, err := database.GetCommandByID(commandID) + if err != nil { + return + } + dLat, _ := cmd["dest_latitude"].(float64) + dLon, _ := cmd["dest_longitude"].(float64) + if dLat != 0 && dLon != 0 { + return // coords déjà présentes + } + adresse, _ := cmd["adresse"].(string) + if adresse == "" { + return + } + location, err := geoService.GeocodeAddress(adresse) + if err != nil || location == nil { + log.Printf("⚠️ [ASSIGN] Géocodage échoué pour cmd %d: %v", commandID, err) + return + } + coordsJSON, _ := json.Marshal(map[string]float64{ + "lat": location.Latitude, + "lon": location.Longitude, + }) + destKey := fmt.Sprintf("command:destination:%d", commandID) + db.Redis.Set(db.RedisCtx, destKey, coordsJSON, 4*time.Hour) + database.GDB.Exec( + "UPDATE commandes SET dest_latitude = ?, dest_longitude = ? WHERE id = ?", + location.Latitude, location.Longitude, commandID, + ) + log.Printf("📍 [ASSIGN] Coords stockées pour cmd %d: (%.6f, %.6f)", commandID, location.Latitude, location.Longitude) + }() + log.Printf("✅ [ASSIGN] Commande %d assignée à %s", commandID, livreurUsername) c.JSON(http.StatusOK, gin.H{ diff --git a/backend/gestion/handlers/deleviry.go b/backend/gestion/handlers/deleviry.go index 81546cc2..3d8d8fca 100644 --- a/backend/gestion/handlers/deleviry.go +++ b/backend/gestion/handlers/deleviry.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "gestion/db" + "gestion/services" "gestion/utils" "log" "net/http" @@ -323,28 +324,53 @@ func UpdateDeliveryStatus(c *gin.Context) { } if destLat != 0 && destLon != 0 { - etaMinutes = database.CalculateETAForDeliveryman(usernameStr, destLat, destLon) + toCoords := services.Coordinates{Latitude: destLat, Longitude: destLon} - if err := database.SetCommandETA(commandID, etaMinutes); err != nil { - log.Printf("⚠️ [STATUS_LIVREUR] Erreur définition ETA: %v", err) + // Cas 1 : GPS du livreur disponible + gpsLat, gpsLon, gpsErr := database.GetDeliveryPersonLocation(usernameStr) + if gpsErr == nil && gpsLat != 0 { + from := services.Coordinates{Latitude: gpsLat, Longitude: gpsLon} + eta, _, err := services.GetETAWithTraffic(from, toCoords) + if err != nil { + eta = services.CalculateETA(services.CalculateDistance(from, toCoords)) + } + etaMinutes = eta + log.Printf("📍 [STATUS_LIVREUR] ETA depuis GPS livreur: %d min", etaMinutes) } else { - log.Printf("✅ [STATUS_LIVREUR] ETA défini: %d minutes", etaMinutes) - if etaMinutes >= 60 { - h := etaMinutes / 60 - m := etaMinutes % 60 - if m > 0 { - etaMessage = fmt.Sprintf("Arrivée prévue dans %dh%02d", h, m) - } else { - etaMessage = fmt.Sprintf("Arrivée prévue dans %dh", h) + // Cas 2 : GPS absent → dernière adresse de livraison + lastLat, lastLon, lastErr := database.GetLastDeliveryCoords(usernameStr) + if lastErr == nil && lastLat != 0 { + from := services.Coordinates{Latitude: lastLat, Longitude: lastLon} + eta, _, err := services.GetETAWithTraffic(from, toCoords) + if err != nil { + eta = services.CalculateETA(services.CalculateDistance(from, toCoords)) } + etaMinutes = eta + log.Printf("📍 [STATUS_LIVREUR] ETA depuis dernière livraison: %d min", etaMinutes) } else { - etaMessage = fmt.Sprintf("Arrivée prévue dans %d minutes", etaMinutes) + // Cas 3 : Aucune position disponible + etaMinutes = 30 + log.Printf("⚠️ [STATUS_LIVREUR] Aucune position disponible - ETA par défaut: %d min", etaMinutes) } } } else { - log.Printf("⚠️ [STATUS_LIVREUR] Coordonnées destination manquantes - ETA par défaut") etaMinutes = 30 - database.SetCommandETA(commandID, etaMinutes) + log.Printf("⚠️ [STATUS_LIVREUR] Coordonnées destination manquantes - ETA par défaut: %d min", etaMinutes) + } + + database.SetCommandETA(commandID, etaMinutes) + log.Printf("✅ [STATUS_LIVREUR] ETA défini: %d minutes", etaMinutes) + + if etaMinutes >= 60 { + h := etaMinutes / 60 + m := etaMinutes % 60 + if m > 0 { + etaMessage = fmt.Sprintf("Arrivée prévue dans %dh%02d", h, m) + } else { + etaMessage = fmt.Sprintf("Arrivée prévue dans %dh", h) + } + } else { + etaMessage = fmt.Sprintf("Arrivée prévue dans %d minutes", etaMinutes) } // Mettre à jour le statut du livreur en "delivering" diff --git a/backend/gestion/handlers/eta.go b/backend/gestion/handlers/eta.go index d385db66..ce650012 100644 --- a/backend/gestion/handlers/eta.go +++ b/backend/gestion/handlers/eta.go @@ -13,6 +13,42 @@ import ( "github.com/gin-gonic/gin" ) +// returnStaleOrUnavailable retourne le cache périmé avec le temps restant recalculé, +// ou {eta_available: false, message: "Aucune heure disponible"} si le cache est absent ou expiré. +func returnStaleOrUnavailable(commandID int, status string, etaData map[string]string) gin.H { + if len(etaData) > 0 { + if updatedAtStr, ok := etaData["updated_at"]; ok { + var updatedAt int64 + fmt.Sscanf(updatedAtStr, "%d", &updatedAt) + var etaMin int64 + if etaStr, ok2 := etaData["eta_minutes"]; ok2 { + fmt.Sscanf(etaStr, "%d", &etaMin) + } + elapsed := int64(time.Since(time.Unix(updatedAt, 0)).Minutes()) + remaining := etaMin - elapsed + if remaining > 0 { + arrival := time.Now().Add(time.Duration(remaining) * time.Minute) + log.Printf("📦 [ETA] Cache périmé utilisé - %d min restantes", remaining) + return gin.H{ + "success": true, + "command_id": commandID, + "status": status, + "eta_minutes": remaining, + "estimated_arrival": arrival.Format("15:04"), + "eta_available": true, + } + } + } + } + return gin.H{ + "success": true, + "command_id": commandID, + "status": status, + "eta_available": false, + "message": "Aucune heure disponible", + } +} + func GetOrderETA(c *gin.Context) { database := c.MustGet("database").(*db.Database) geoService := c.MustGet("geoService").(*services.GeoService) @@ -191,11 +227,8 @@ func GetOrderETA(c *gin.Context) { } if destLat == 0 || destLon == 0 { - log.Printf("❌ [ETA] Coordonnées destination manquantes") - c.JSON(http.StatusBadRequest, gin.H{ - "success": false, - "error": "Coordonnées de destination manquantes", - }) + log.Printf("⚠️ [ETA] Coordonnées destination manquantes - retour cache périmé ou message") + c.JSON(http.StatusOK, returnStaleOrUnavailable(commandID, cmdStatus, etaData)) return } @@ -203,25 +236,33 @@ func GetOrderETA(c *gin.Context) { livreurAssign, _ := command["livreur_assign"].(string) if livreurAssign == "" { log.Printf("⚠️ [ETA] Aucun livreur assigné") - c.JSON(http.StatusBadRequest, gin.H{ - "success": false, - "error": "Aucun livreur assigné à cette commande", - }) - return - } - - livreurLocation, err := geoService.GetDeliveryPersonLocation(livreurAssign) - if err != nil { - log.Printf("❌ [ETA] Position livreur introuvable: %s", livreurAssign) - c.JSON(http.StatusNotFound, gin.H{ - "success": false, - "error": "Position du livreur non disponible", + c.JSON(http.StatusOK, gin.H{ + "success": true, + "command_id": commandID, + "status": cmdStatus, + "eta_available": false, + "message": "Aucune heure disponible", }) return } toCoords := services.Coordinates{Latitude: destLat, Longitude: destLon} + // Cas 1 : GPS livreur disponible + livreurLocation, gpsErr := geoService.GetDeliveryPersonLocation(livreurAssign) + if gpsErr != nil { + // Cas 2 : GPS absent → dernière adresse de livraison + lastLat, lastLon, lastErr := database.GetLastDeliveryCoords(livreurAssign) + if lastErr != nil || lastLat == 0 { + // Cas 3 : Aucune position → cache périmé ou message + log.Printf("⚠️ [ETA] Aucune position disponible pour %s", livreurAssign) + c.JSON(http.StatusOK, returnStaleOrUnavailable(commandID, cmdStatus, etaData)) + return + } + livreurLocation = &services.Coordinates{Latitude: lastLat, Longitude: lastLon} + log.Printf("📍 [ETA] Position depuis dernière livraison: (%.6f, %.6f)", lastLat, lastLon) + } + // Calculer ETA avec TomTom log.Printf("🛣️ [ETA] Calcul TomTom: (%.6f, %.6f) -> (%.6f, %.6f)", livreurLocation.Latitude, livreurLocation.Longitude, toCoords.Latitude, toCoords.Longitude) diff --git a/frontend-prep/src/pages/User/SuiviLivraison.tsx b/frontend-prep/src/pages/User/SuiviLivraison.tsx index 93afdb7d..8c46ba28 100644 --- a/frontend-prep/src/pages/User/SuiviLivraison.tsx +++ b/frontend-prep/src/pages/User/SuiviLivraison.tsx @@ -102,7 +102,9 @@ const getDeliveryAddress = (order: OrderWithTracking): string => { const formatOrderItem = (item: OrderItem) => { return { - name: String(item.produit || item.product_name || item.name_product || "Produit"), + name: String( + item.produit || item.product_name || item.name_product || "Produit", + ), quantity: Number(item.quantite || item.quantity || 0), price: Number(item.prix || item.price || 0), }; @@ -157,7 +159,6 @@ const getStatusIcon = (status: string): IconDefinition => { return iconMap[status?.toLowerCase()] || faQuestionCircle; }; - /** * ✅ Calculer les points avec le TOTAL (pas item par item) */ @@ -335,33 +336,35 @@ function SuiviLivraison() { if (response.success) { const ordersWithTracking = await Promise.all( - (response.commands || []).map(async (order: OrderDetail) => { - const normalizedOrder = { - ...order, - total: getTotalAmount(order), - }; + (response.commands || []).map( + async (order: OrderDetail) => { + const normalizedOrder = { + ...order, + total: getTotalAmount(order), + }; - let tracking; - let eta; + let tracking; + let eta; - try { - tracking = await getOrderTracking(order.id); - } catch { - tracking = undefined; - } + try { + tracking = await getOrderTracking(order.id); + } catch { + tracking = undefined; + } - try { - eta = await getOrderETA(order.id); - } catch { - eta = undefined; - } + try { + eta = await getOrderETA(order.id); + } catch { + eta = undefined; + } - return { - ...normalizedOrder, - tracking, - eta, - }; - }), + return { + ...normalizedOrder, + tracking, + eta, + }; + }, + ), ); setOrders(ordersWithTracking); @@ -369,7 +372,10 @@ function SuiviLivraison() { } } catch (err: unknown) { console.error("Erreur loadOrders:", err); - const msg = err instanceof Error ? err.message : "Erreur lors du chargement"; + const msg = + err instanceof Error + ? err.message + : "Erreur lors du chargement"; setError(msg); showToast(msg, "error"); } finally { @@ -400,7 +406,10 @@ function SuiviLivraison() { const order = orders.find((o) => o.id === orderId); if (order) { - const { points, categoryDisplay } = calculateOrderPoints(order, poolNames); + const { points, categoryDisplay } = calculateOrderPoints( + order, + poolNames, + ); setSelectedOrderPoints(points); setSelectedOrderCategoryDisplay(categoryDisplay); } else { @@ -454,11 +463,6 @@ function SuiviLivraison() { ); setConfirming(null); loadOrders(); - const confirmedOrder = orders.find((o) => o.id === orderToConfirm); - navigate( - `/user/commande/${confirmedOrder?.client_order_number ?? orderToConfirm}`, - { state: { commandId: orderToConfirm } }, - ); } else { setError(response.message || "Erreur lors de la confirmation"); showToast( @@ -554,7 +558,12 @@ function SuiviLivraison() { } } catch (error: unknown) { console.error("❌ [CANCEL] Erreur:", error); - showToast(error instanceof Error ? error.message : "Erreur lors de l'annulation", "error"); + showToast( + error instanceof Error + ? error.message + : "Erreur lors de l'annulation", + "error", + ); } finally { setCancellingOrder(null); } @@ -622,10 +631,19 @@ function SuiviLivraison() { // Heure d'arrivée estimée : depuis le backend ou calculée côté client const computedArrival = (() => { if (statusLow !== "en_route") return null; - if (order.eta?.estimated_arrival) return order.eta.estimated_arrival; - if (order.eta?.eta_minutes && order.eta.eta_minutes > 0) { - return new Date(Date.now() + order.eta.eta_minutes * 60000) - .toLocaleTimeString("fr-FR", { hour: "2-digit", minute: "2-digit" }); + if (order.eta?.estimated_arrival) + return order.eta.estimated_arrival; + if ( + order.eta?.eta_minutes && + order.eta.eta_minutes > 0 + ) { + return new Date( + Date.now() + + order.eta.eta_minutes * 60000, + ).toLocaleTimeString("fr-FR", { + hour: "2-digit", + minute: "2-digit", + }); } return null; })(); @@ -660,7 +678,8 @@ function SuiviLivraison() {
- Commande #{order.client_order_number} + Commande # + {order.client_order_number}
- {(showEta || (statusLow === "en_route" && computedArrival)) && ( + {statusLow === "en_route" && (
- - {` Vers ${computedArrival}`} + + {computedArrival + ? ` Vers ${computedArrival}` + : " Aucune heure disponible"}
)} {showArrivedEta && (
- + {" ~5 min"}
)} @@ -767,40 +792,80 @@ function SuiviLivraison() { {showArrivedEta && (
- +
-

Livreur sur place

-

~5 min

-

Préparez-vous à réceptionner votre commande

+

+ Livreur sur place +

+

+ ~5 min +

+

+ Préparez-vous à + réceptionner votre + commande +

)} {/* ETA card - en_route */} - {(showEta || (statusLow === "en_route" && computedArrival)) && ( -
+ {(showEta || + (statusLow === "en_route" && + computedArrival)) && ( +
- +
-

Heure d'arrivée estimée

+

+ Heure d'arrivée + estimée +

{computedArrival && ( -

{computedArrival}

- )} - {order.eta?.eta_minutes && order.eta.eta_minutes > 0 && ( -

- ~{order.eta.eta_minutes} min restantes +

+ { + computedArrival + }

)} - {order.eta?.livreur_distance != null && ( + {order.eta + ?.eta_minutes && + order.eta + .eta_minutes > + 0 && ( +

+ ~ + { + order + .eta + .eta_minutes + }{" "} + min + restantes +

+ )} + {order.eta + ?.livreur_distance != + null && (

Distance :{" "} - {typeof order.eta.livreur_distance === "number" - ? order.eta.livreur_distance.toFixed(1) - : order.eta.livreur_distance}{" "} + {typeof order + .eta + .livreur_distance === + "number" + ? order.eta.livreur_distance.toFixed( + 1, + ) + : order.eta + .livreur_distance}{" "} km

)} @@ -927,19 +992,53 @@ function SuiviLivraison() { />{" "} Montant total - {(order.referral_used ?? 0) > 0 && ( -

- Brut : {(order.total_prix ?? 0).toFixed(2)} € + {(order.referral_used ?? + 0) > 0 && ( +

+ Brut :{" "} + {( + order.total_prix ?? + 0 + ).toFixed(2)}{" "} + €

)}

- {Math.max(0, getTotalAmount(order) - (order.referral_used ?? 0)).toFixed(2)} € + {Math.max( + 0, + getTotalAmount( + order, + ) - + (order.referral_used ?? + 0), + ).toFixed(2)}{" "} + €

- {(order.referral_used ?? 0) > 0 && ( -

- — dont {(order.referral_used!).toFixed(2)} € parrainage déduit + {(order.referral_used ?? + 0) > 0 && ( +

+ — dont{" "} + {order.referral_used!.toFixed( + 2, + )}{" "} + € parrainage déduit

)}
diff --git a/mobile/src/screens/client/OrderTrackingScreen.tsx b/mobile/src/screens/client/OrderTrackingScreen.tsx index 568493a1..9d2c37f2 100644 --- a/mobile/src/screens/client/OrderTrackingScreen.tsx +++ b/mobile/src/screens/client/OrderTrackingScreen.tsx @@ -20,6 +20,7 @@ import { formatOrderDate, formatPrice, calculateOrderTotal, + getPublicSettings, } from "../../api/api"; import type { OrderDetail, @@ -72,6 +73,7 @@ export default function OrderTrackingScreen() { const [penaltyWarning, setPenaltyWarning] = useState(null); const [penaltyOrderId, setPenaltyOrderId] = useState(null); + const [penaltiesEnabled, setPenaltiesEnabled] = useState(false); const [toastMsg, setToastMsg] = useState(""); const [toastType, setToastType] = useState< "success" | "error" | "warning" | "info" @@ -98,6 +100,12 @@ export default function OrderTrackingScreen() { } }, []); + useEffect(() => { + getPublicSettings() + .then((s) => setPenaltiesEnabled(s.penalties_enabled)) + .catch(() => {}); + }, []); + useFocusEffect( useCallback(() => { setLoading(true); @@ -140,7 +148,6 @@ export default function OrderTrackingScreen() { "success", ); fetchOrders(); - navigation.navigate("OrderDetails", { orderId }); } else { showToast(res.message || "Erreur", "error"); } @@ -274,21 +281,6 @@ export default function OrderTrackingScreen() { gap: spacing.s, marginTop: spacing.l, }, - confirmBanner: { - flexDirection: "row", - alignItems: "center", - justifyContent: "center", - gap: spacing.s, - backgroundColor: colors.success, - borderRadius: borderRadius.md, - paddingVertical: spacing.m, - marginTop: spacing.m, - }, - confirmBannerText: { - color: "#fff", - fontWeight: "700", - fontSize: fontSize.sm, - }, proposalBox: { backgroundColor: colors.warning + "18", borderWidth: 1, @@ -402,7 +394,10 @@ export default function OrderTrackingScreen() { renderItem={({ item: order }) => { const expanded = expandedId === order.id; const gross = calculateOrderTotal(order); - const total = Math.max(0, gross - (order.referral_used ?? 0)); + const total = Math.max( + 0, + gross - (order.referral_used ?? 0), + ); const progress = STATUS_PROGRESS[order.status] || 0; const track = tracking[order.id]; const eta = etas[order.id]; @@ -466,8 +461,18 @@ export default function OrderTrackingScreen() { {formatPrice(total)} {(order.referral_used ?? 0) > 0 && ( - - dont -{(order.referral_used as number).toFixed(2)} € parrainage + + dont - + {( + order.referral_used as number + ).toFixed(2)}{" "} + € parrainage )} @@ -481,16 +486,6 @@ export default function OrderTrackingScreen() { color={colors.textMuted} /> - {canConfirm && ( - setConfirmingId(order.id)} - style={styles.confirmBanner} - > - - Valider la réception - - )} {order.address_proposal_status === "pending" && order.proposed_address && ( @@ -571,9 +566,7 @@ export default function OrderTrackingScreen() { )} - {order.status === "en_route" && - eta?.eta_minutes != null && - eta.eta_minutes > 0 && ( + {order.status === "en_route" && ( - Temps de livraison estimé : ~{eta.eta_minutes} min + {eta?.eta_minutes != null && + eta.eta_minutes > 0 + ? `Temps de livraison estimé : ~${eta.eta_minutes} min` + : "Aucune heure disponible"} )} @@ -593,7 +589,8 @@ export default function OrderTrackingScreen() { color={colors.warning} /> - Temps de livraison estimé : ~ + Temps de livraison estimé : + ~ {eta?.eta_minutes != null && eta.eta_minutes > 0 && eta.eta_minutes < 5 @@ -627,6 +624,18 @@ export default function OrderTrackingScreen() { variant="outline" size="sm" /> + {canConfirm && ( +