chore: build
Backend - Build & Lint / build (push) Has been cancelled
Frontend Client - EAS Build / build (push) Has been cancelled
Frontend Web - Build & Lint / build (push) Has been cancelled

This commit is contained in:
2026-06-30 19:24:36 +02:00
parent 781212f706
commit 8d131a1ade
6 changed files with 420 additions and 139 deletions
+32 -1
View File
@@ -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 {
+37
View File
@@ -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{
+35 -9
View File
@@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"gestion/db"
"gestion/services"
"gestion/utils"
"log"
"net/http"
@@ -323,12 +324,43 @@ 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 {
// 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 {
// Cas 3 : Aucune position disponible
etaMinutes = 30
log.Printf("⚠️ [STATUS_LIVREUR] Aucune position disponible - ETA par défaut: %d min", etaMinutes)
}
}
} else {
etaMinutes = 30
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
@@ -340,12 +372,6 @@ func UpdateDeliveryStatus(c *gin.Context) {
} else {
etaMessage = fmt.Sprintf("Arrivée prévue dans %d minutes", etaMinutes)
}
}
} else {
log.Printf("⚠️ [STATUS_LIVREUR] Coordonnées destination manquantes - ETA par défaut")
etaMinutes = 30
database.SetCommandETA(commandID, etaMinutes)
}
// Mettre à jour le statut du livreur en "delivering"
database.SetDeliveryPersonStatus(usernameStr, "delivering", commandID)
+59 -18
View File
@@ -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)
+146 -47
View File
@@ -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,7 +336,8 @@ function SuiviLivraison() {
if (response.success) {
const ordersWithTracking = await Promise.all(
(response.commands || []).map(async (order: OrderDetail) => {
(response.commands || []).map(
async (order: OrderDetail) => {
const normalizedOrder = {
...order,
total: getTotalAmount(order),
@@ -361,7 +363,8 @@ function SuiviLivraison() {
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() {
<div className="order-header-left">
<div className="order-id-row">
<span className="order-id">
Commande #{order.client_order_number}
Commande #
{order.client_order_number}
</span>
<div
className="status-badge"
@@ -680,15 +699,21 @@ function SuiviLivraison() {
order.status,
)}
</div>
{(showEta || (statusLow === "en_route" && computedArrival)) && (
{statusLow === "en_route" && (
<div className="eta-badge eta-badge--enRoute">
<FontAwesomeIcon icon={faClock} />
{` Vers ${computedArrival}`}
<FontAwesomeIcon
icon={faClock}
/>
{computedArrival
? ` Vers ${computedArrival}`
: " Aucune heure disponible"}
</div>
)}
{showArrivedEta && (
<div className="eta-badge eta-badge--arrived">
<FontAwesomeIcon icon={faClock} />
<FontAwesomeIcon
icon={faClock}
/>
{" ~5 min"}
</div>
)}
@@ -767,40 +792,80 @@ function SuiviLivraison() {
{showArrivedEta && (
<div className="eta-card eta-card--arrived">
<div className="eta-card-icon">
<FontAwesomeIcon icon={faMapMarkerAlt} />
<FontAwesomeIcon
icon={
faMapMarkerAlt
}
/>
</div>
<div className="eta-card-body">
<p className="eta-card-title">Livreur sur place</p>
<p className="eta-card-value">~5 min</p>
<p className="eta-card-sub">Préparez-vous à réceptionner votre commande</p>
<p className="eta-card-title">
Livreur sur place
</p>
<p className="eta-card-value">
~5 min
</p>
<p className="eta-card-sub">
Préparez-vous à
réceptionner votre
commande
</p>
</div>
</div>
)}
{/* ETA card - en_route */}
{(showEta || (statusLow === "en_route" && computedArrival)) && (
<div
className="eta-card eta-card--enRoute"
>
{(showEta ||
(statusLow === "en_route" &&
computedArrival)) && (
<div className="eta-card eta-card--enRoute">
<div className="eta-card-icon">
<FontAwesomeIcon icon={faClock} />
<FontAwesomeIcon
icon={faClock}
/>
</div>
<div className="eta-card-body">
<p className="eta-card-title">Heure d'arrivée estimée</p>
<p className="eta-card-title">
Heure d'arrivée
estimée
</p>
{computedArrival && (
<p className="eta-card-value">{computedArrival}</p>
)}
{order.eta?.eta_minutes && order.eta.eta_minutes > 0 && (
<p className="eta-card-sub">
~{order.eta.eta_minutes} min restantes
<p className="eta-card-value">
{
computedArrival
}
</p>
)}
{order.eta?.livreur_distance != null && (
{order.eta
?.eta_minutes &&
order.eta
.eta_minutes >
0 && (
<p className="eta-card-sub">
~
{
order
.eta
.eta_minutes
}{" "}
min
restantes
</p>
)}
{order.eta
?.livreur_distance !=
null && (
<p className="eta-card-sub">
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
</p>
)}
@@ -927,19 +992,53 @@ function SuiviLivraison() {
/>{" "}
Montant total
</h4>
{(order.referral_used ?? 0) > 0 && (
<p style={{ margin: "0 0 2px", fontSize: "0.85rem", color: "var(--text-muted)" }}>
Brut : {(order.total_prix ?? 0).toFixed(2)}
{(order.referral_used ??
0) > 0 && (
<p
style={{
margin: "0 0 2px",
fontSize:
"0.85rem",
color: "var(--text-muted)",
}}
>
Brut :{" "}
{(
order.total_prix ??
0
).toFixed(2)}{" "}
</p>
)}
<p className="total-amount">
<strong>
{Math.max(0, getTotalAmount(order) - (order.referral_used ?? 0)).toFixed(2)}
{Math.max(
0,
getTotalAmount(
order,
) -
(order.referral_used ??
0),
).toFixed(2)}{" "}
</strong>
</p>
{(order.referral_used ?? 0) > 0 && (
<p style={{ margin: "4px 0 0", fontSize: "0.82rem", color: "#10b981", fontWeight: 500 }}>
dont {(order.referral_used!).toFixed(2)} parrainage déduit
{(order.referral_used ??
0) > 0 && (
<p
style={{
margin: "4px 0 0",
fontSize:
"0.82rem",
color: "#10b981",
fontWeight: 500,
}}
>
dont{" "}
{order.referral_used!.toFixed(
2,
)}{" "}
parrainage déduit
</p>
)}
</div>
@@ -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<CancelCommandResponse | null>(null);
const [penaltyOrderId, setPenaltyOrderId] = useState<number | null>(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)}
</Text>
{(order.referral_used ?? 0) > 0 && (
<Text style={{ fontSize: 11, color: colors.success, marginTop: 2 }}>
dont -{(order.referral_used as number).toFixed(2)} parrainage
<Text
style={{
fontSize: 11,
color: colors.success,
marginTop: 2,
}}
>
dont -
{(
order.referral_used as number
).toFixed(2)}{" "}
parrainage
</Text>
)}
</View>
@@ -481,16 +486,6 @@ export default function OrderTrackingScreen() {
color={colors.textMuted}
/>
</View>
{canConfirm && (
<TouchableOpacity
activeOpacity={0.85}
onPress={() => setConfirmingId(order.id)}
style={styles.confirmBanner}
>
<Ionicons name="checkmark-circle-outline" size={18} color="#fff" />
<Text style={styles.confirmBannerText}>Valider la réception</Text>
</TouchableOpacity>
)}
{order.address_proposal_status === "pending" &&
order.proposed_address && (
<View style={styles.proposalBox}>
@@ -571,9 +566,7 @@ export default function OrderTrackingScreen() {
</Text>
</View>
)}
{order.status === "en_route" &&
eta?.eta_minutes != null &&
eta.eta_minutes > 0 && (
{order.status === "en_route" && (
<View style={styles.trackRow}>
<Ionicons
name="timer-outline"
@@ -581,7 +574,10 @@ export default function OrderTrackingScreen() {
color={colors.warning}
/>
<Text style={styles.trackText}>
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"}
</Text>
</View>
)}
@@ -593,7 +589,8 @@ export default function OrderTrackingScreen() {
color={colors.warning}
/>
<Text style={styles.trackText}>
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 && (
<Button
title="Confirmer reception"
onPress={() =>
setConfirmingId(
order.id,
)
}
variant="success"
size="sm"
/>
)}
{canCancel && (
<Button
title="Annuler"
@@ -691,6 +700,39 @@ export default function OrderTrackingScreen() {
iconColor={colors.danger}
>
<View style={styles.modalBody}>
{penaltiesEnabled && (
<View
style={{
flexDirection: "row",
alignItems: "flex-start",
gap: 8,
backgroundColor: colors.danger + "18",
borderWidth: 1,
borderColor: colors.danger + "55",
borderRadius: 8,
padding: 12,
marginBottom: 12,
}}
>
<Ionicons
name="warning-outline"
size={18}
color={colors.danger}
style={{ marginTop: 1 }}
/>
<Text
style={{
flex: 1,
color: colors.danger,
fontSize: 13,
lineHeight: 18,
}}
>
Attention : en cas d'annulations répétées, une
amende sera appliquée à votre compte.
</Text>
</View>
)}
<Text style={styles.modalText}>
Raison de l'annulation :
</Text>
@@ -753,12 +795,17 @@ export default function OrderTrackingScreen() {
<Ionicons
name="remove-circle-outline"
size={16}
color={colors.warning}
color={colors.danger}
/>
<Text style={styles.penaltyDetail}>
Penalite:{" "}
<Text
style={[
styles.penaltyDetail,
{ color: colors.danger, fontWeight: "700" },
]}
>
Amende :{" "}
{penaltyWarning.penalty_warning.penalty_amount}{" "}
points
</Text>
</View>
)}