chore: build
This commit is contained in:
@@ -374,13 +374,17 @@ func (d *Database) GetCommandByID(id int) (map[string]any, error) {
|
|||||||
ReferralUsed float64 `gorm:"column:referral_used"`
|
ReferralUsed float64 `gorm:"column:referral_used"`
|
||||||
ClientOrderNumber int `gorm:"column:client_order_number"`
|
ClientOrderNumber int `gorm:"column:client_order_number"`
|
||||||
CancelReason string `gorm:"column:cancel_reason"`
|
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").
|
if err := d.GDB.Table("commandes c").
|
||||||
Select(`c.id, c.username, c.status, c.adresse, c.total_prix, c.livreur_assign,
|
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.created_at, c.updated_at, c.proposed_address, c.address_proposal_status,
|
||||||
c.referral_used, c.client_order_id AS client_order_number,
|
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).
|
Where("c.id = ?", id).
|
||||||
First(&row).Error; err != nil {
|
First(&row).Error; err != nil {
|
||||||
return nil, fmt.Errorf("erreur lors de la récupération de la commande: %w", err)
|
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,
|
"referral_used": row.ReferralUsed,
|
||||||
"client_order_number": row.ClientOrderNumber,
|
"client_order_number": row.ClientOrderNumber,
|
||||||
"cancel_reason": row.CancelReason,
|
"cancel_reason": row.CancelReason,
|
||||||
|
"dest_latitude": row.DestLatitude,
|
||||||
|
"dest_longitude": row.DestLongitude,
|
||||||
}
|
}
|
||||||
|
|
||||||
if row.LivreurAssign != nil {
|
if row.LivreurAssign != nil {
|
||||||
@@ -418,6 +424,31 @@ func (d *Database) GetCommandByID(id int) (map[string]any, error) {
|
|||||||
return command, nil
|
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.
|
// GetClientOrderID retourne le client_order_id (numéro perso du client) pour un commandID global.
|
||||||
// Retourne commandID en fallback si introuvable.
|
// Retourne commandID en fallback si introuvable.
|
||||||
func (d *Database) GetClientOrderID(commandID int) int {
|
func (d *Database) GetClientOrderID(commandID int) int {
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ package handlers
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/csv"
|
"encoding/csv"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"gestion/db"
|
"gestion/db"
|
||||||
|
"gestion/services"
|
||||||
"gestion/utils"
|
"gestion/utils"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"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) {
|
func ProposeAddressChange(c *gin.Context) {
|
||||||
database := c.MustGet("database").(*db.Database)
|
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": "..."})
|
// Cabine: POST /api/v1/cabine/commands/:id/assign (body: {"livreur_username": "..."})
|
||||||
func AssignDeliveryPerson(c *gin.Context) {
|
func AssignDeliveryPerson(c *gin.Context) {
|
||||||
database := c.MustGet("database").(*db.Database)
|
database := c.MustGet("database").(*db.Database)
|
||||||
|
geoService := c.MustGet("geoService").(*services.GeoService)
|
||||||
|
|
||||||
// ✅ SÉCURITÉ: Admin ou Cabine
|
// ✅ SÉCURITÉ: Admin ou Cabine
|
||||||
role := c.GetString("role")
|
role := c.GetString("role")
|
||||||
@@ -678,6 +682,39 @@ func AssignDeliveryPerson(c *gin.Context) {
|
|||||||
fmt.Sprintf("Livreur '%s' assigné manuellement par %s", livreurUsername, staffUsername),
|
fmt.Sprintf("Livreur '%s' assigné manuellement par %s", livreurUsername, staffUsername),
|
||||||
staffUsername.(string))
|
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)
|
log.Printf("✅ [ASSIGN] Commande %d assignée à %s", commandID, livreurUsername)
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"gestion/db"
|
"gestion/db"
|
||||||
|
"gestion/services"
|
||||||
"gestion/utils"
|
"gestion/utils"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -323,12 +324,43 @@ func UpdateDeliveryStatus(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if destLat != 0 && destLon != 0 {
|
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 {
|
// Cas 1 : GPS du livreur disponible
|
||||||
log.Printf("⚠️ [STATUS_LIVREUR] Erreur définition ETA: %v", err)
|
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 {
|
} 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)
|
log.Printf("✅ [STATUS_LIVREUR] ETA défini: %d minutes", etaMinutes)
|
||||||
|
|
||||||
if etaMinutes >= 60 {
|
if etaMinutes >= 60 {
|
||||||
h := etaMinutes / 60
|
h := etaMinutes / 60
|
||||||
m := etaMinutes % 60
|
m := etaMinutes % 60
|
||||||
@@ -340,12 +372,6 @@ func UpdateDeliveryStatus(c *gin.Context) {
|
|||||||
} else {
|
} else {
|
||||||
etaMessage = fmt.Sprintf("Arrivée prévue dans %d minutes", etaMinutes)
|
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"
|
// Mettre à jour le statut du livreur en "delivering"
|
||||||
database.SetDeliveryPersonStatus(usernameStr, "delivering", commandID)
|
database.SetDeliveryPersonStatus(usernameStr, "delivering", commandID)
|
||||||
|
|||||||
@@ -13,6 +13,42 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"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) {
|
func GetOrderETA(c *gin.Context) {
|
||||||
database := c.MustGet("database").(*db.Database)
|
database := c.MustGet("database").(*db.Database)
|
||||||
geoService := c.MustGet("geoService").(*services.GeoService)
|
geoService := c.MustGet("geoService").(*services.GeoService)
|
||||||
@@ -191,11 +227,8 @@ func GetOrderETA(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if destLat == 0 || destLon == 0 {
|
if destLat == 0 || destLon == 0 {
|
||||||
log.Printf("❌ [ETA] Coordonnées destination manquantes")
|
log.Printf("⚠️ [ETA] Coordonnées destination manquantes - retour cache périmé ou message")
|
||||||
c.JSON(http.StatusBadRequest, gin.H{
|
c.JSON(http.StatusOK, returnStaleOrUnavailable(commandID, cmdStatus, etaData))
|
||||||
"success": false,
|
|
||||||
"error": "Coordonnées de destination manquantes",
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,25 +236,33 @@ func GetOrderETA(c *gin.Context) {
|
|||||||
livreurAssign, _ := command["livreur_assign"].(string)
|
livreurAssign, _ := command["livreur_assign"].(string)
|
||||||
if livreurAssign == "" {
|
if livreurAssign == "" {
|
||||||
log.Printf("⚠️ [ETA] Aucun livreur assigné")
|
log.Printf("⚠️ [ETA] Aucun livreur assigné")
|
||||||
c.JSON(http.StatusBadRequest, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": true,
|
||||||
"error": "Aucun livreur assigné à cette commande",
|
"command_id": commandID,
|
||||||
})
|
"status": cmdStatus,
|
||||||
return
|
"eta_available": false,
|
||||||
}
|
"message": "Aucune heure disponible",
|
||||||
|
|
||||||
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",
|
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
toCoords := services.Coordinates{Latitude: destLat, Longitude: destLon}
|
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
|
// Calculer ETA avec TomTom
|
||||||
log.Printf("🛣️ [ETA] Calcul TomTom: (%.6f, %.6f) -> (%.6f, %.6f)",
|
log.Printf("🛣️ [ETA] Calcul TomTom: (%.6f, %.6f) -> (%.6f, %.6f)",
|
||||||
livreurLocation.Latitude, livreurLocation.Longitude, toCoords.Latitude, toCoords.Longitude)
|
livreurLocation.Latitude, livreurLocation.Longitude, toCoords.Latitude, toCoords.Longitude)
|
||||||
|
|||||||
@@ -102,7 +102,9 @@ const getDeliveryAddress = (order: OrderWithTracking): string => {
|
|||||||
|
|
||||||
const formatOrderItem = (item: OrderItem) => {
|
const formatOrderItem = (item: OrderItem) => {
|
||||||
return {
|
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),
|
quantity: Number(item.quantite || item.quantity || 0),
|
||||||
price: Number(item.prix || item.price || 0),
|
price: Number(item.prix || item.price || 0),
|
||||||
};
|
};
|
||||||
@@ -157,7 +159,6 @@ const getStatusIcon = (status: string): IconDefinition => {
|
|||||||
return iconMap[status?.toLowerCase()] || faQuestionCircle;
|
return iconMap[status?.toLowerCase()] || faQuestionCircle;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ✅ Calculer les points avec le TOTAL (pas item par item)
|
* ✅ Calculer les points avec le TOTAL (pas item par item)
|
||||||
*/
|
*/
|
||||||
@@ -335,7 +336,8 @@ function SuiviLivraison() {
|
|||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
const ordersWithTracking = await Promise.all(
|
const ordersWithTracking = await Promise.all(
|
||||||
(response.commands || []).map(async (order: OrderDetail) => {
|
(response.commands || []).map(
|
||||||
|
async (order: OrderDetail) => {
|
||||||
const normalizedOrder = {
|
const normalizedOrder = {
|
||||||
...order,
|
...order,
|
||||||
total: getTotalAmount(order),
|
total: getTotalAmount(order),
|
||||||
@@ -361,7 +363,8 @@ function SuiviLivraison() {
|
|||||||
tracking,
|
tracking,
|
||||||
eta,
|
eta,
|
||||||
};
|
};
|
||||||
}),
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
setOrders(ordersWithTracking);
|
setOrders(ordersWithTracking);
|
||||||
@@ -369,7 +372,10 @@ function SuiviLivraison() {
|
|||||||
}
|
}
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
console.error("Erreur loadOrders:", err);
|
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);
|
setError(msg);
|
||||||
showToast(msg, "error");
|
showToast(msg, "error");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -400,7 +406,10 @@ function SuiviLivraison() {
|
|||||||
const order = orders.find((o) => o.id === orderId);
|
const order = orders.find((o) => o.id === orderId);
|
||||||
|
|
||||||
if (order) {
|
if (order) {
|
||||||
const { points, categoryDisplay } = calculateOrderPoints(order, poolNames);
|
const { points, categoryDisplay } = calculateOrderPoints(
|
||||||
|
order,
|
||||||
|
poolNames,
|
||||||
|
);
|
||||||
setSelectedOrderPoints(points);
|
setSelectedOrderPoints(points);
|
||||||
setSelectedOrderCategoryDisplay(categoryDisplay);
|
setSelectedOrderCategoryDisplay(categoryDisplay);
|
||||||
} else {
|
} else {
|
||||||
@@ -454,11 +463,6 @@ function SuiviLivraison() {
|
|||||||
);
|
);
|
||||||
setConfirming(null);
|
setConfirming(null);
|
||||||
loadOrders();
|
loadOrders();
|
||||||
const confirmedOrder = orders.find((o) => o.id === orderToConfirm);
|
|
||||||
navigate(
|
|
||||||
`/user/commande/${confirmedOrder?.client_order_number ?? orderToConfirm}`,
|
|
||||||
{ state: { commandId: orderToConfirm } },
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
setError(response.message || "Erreur lors de la confirmation");
|
setError(response.message || "Erreur lors de la confirmation");
|
||||||
showToast(
|
showToast(
|
||||||
@@ -554,7 +558,12 @@ function SuiviLivraison() {
|
|||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
console.error("❌ [CANCEL] Erreur:", error);
|
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 {
|
} finally {
|
||||||
setCancellingOrder(null);
|
setCancellingOrder(null);
|
||||||
}
|
}
|
||||||
@@ -622,10 +631,19 @@ function SuiviLivraison() {
|
|||||||
// Heure d'arrivée estimée : depuis le backend ou calculée côté client
|
// Heure d'arrivée estimée : depuis le backend ou calculée côté client
|
||||||
const computedArrival = (() => {
|
const computedArrival = (() => {
|
||||||
if (statusLow !== "en_route") return null;
|
if (statusLow !== "en_route") return null;
|
||||||
if (order.eta?.estimated_arrival) return order.eta.estimated_arrival;
|
if (order.eta?.estimated_arrival)
|
||||||
if (order.eta?.eta_minutes && order.eta.eta_minutes > 0) {
|
return order.eta.estimated_arrival;
|
||||||
return new Date(Date.now() + order.eta.eta_minutes * 60000)
|
if (
|
||||||
.toLocaleTimeString("fr-FR", { hour: "2-digit", minute: "2-digit" });
|
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;
|
return null;
|
||||||
})();
|
})();
|
||||||
@@ -660,7 +678,8 @@ function SuiviLivraison() {
|
|||||||
<div className="order-header-left">
|
<div className="order-header-left">
|
||||||
<div className="order-id-row">
|
<div className="order-id-row">
|
||||||
<span className="order-id">
|
<span className="order-id">
|
||||||
Commande #{order.client_order_number}
|
Commande #
|
||||||
|
{order.client_order_number}
|
||||||
</span>
|
</span>
|
||||||
<div
|
<div
|
||||||
className="status-badge"
|
className="status-badge"
|
||||||
@@ -680,15 +699,21 @@ function SuiviLivraison() {
|
|||||||
order.status,
|
order.status,
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{(showEta || (statusLow === "en_route" && computedArrival)) && (
|
{statusLow === "en_route" && (
|
||||||
<div className="eta-badge eta-badge--enRoute">
|
<div className="eta-badge eta-badge--enRoute">
|
||||||
<FontAwesomeIcon icon={faClock} />
|
<FontAwesomeIcon
|
||||||
{` Vers ${computedArrival}`}
|
icon={faClock}
|
||||||
|
/>
|
||||||
|
{computedArrival
|
||||||
|
? ` Vers ${computedArrival}`
|
||||||
|
: " Aucune heure disponible"}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{showArrivedEta && (
|
{showArrivedEta && (
|
||||||
<div className="eta-badge eta-badge--arrived">
|
<div className="eta-badge eta-badge--arrived">
|
||||||
<FontAwesomeIcon icon={faClock} />
|
<FontAwesomeIcon
|
||||||
|
icon={faClock}
|
||||||
|
/>
|
||||||
{" ~5 min"}
|
{" ~5 min"}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -767,40 +792,80 @@ function SuiviLivraison() {
|
|||||||
{showArrivedEta && (
|
{showArrivedEta && (
|
||||||
<div className="eta-card eta-card--arrived">
|
<div className="eta-card eta-card--arrived">
|
||||||
<div className="eta-card-icon">
|
<div className="eta-card-icon">
|
||||||
<FontAwesomeIcon icon={faMapMarkerAlt} />
|
<FontAwesomeIcon
|
||||||
|
icon={
|
||||||
|
faMapMarkerAlt
|
||||||
|
}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="eta-card-body">
|
<div className="eta-card-body">
|
||||||
<p className="eta-card-title">Livreur sur place</p>
|
<p className="eta-card-title">
|
||||||
<p className="eta-card-value">~5 min</p>
|
Livreur sur place
|
||||||
<p className="eta-card-sub">Préparez-vous à réceptionner votre commande</p>
|
</p>
|
||||||
|
<p className="eta-card-value">
|
||||||
|
~5 min
|
||||||
|
</p>
|
||||||
|
<p className="eta-card-sub">
|
||||||
|
Préparez-vous à
|
||||||
|
réceptionner votre
|
||||||
|
commande
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ETA card - en_route */}
|
{/* ETA card - en_route */}
|
||||||
{(showEta || (statusLow === "en_route" && computedArrival)) && (
|
{(showEta ||
|
||||||
<div
|
(statusLow === "en_route" &&
|
||||||
className="eta-card eta-card--enRoute"
|
computedArrival)) && (
|
||||||
>
|
<div className="eta-card eta-card--enRoute">
|
||||||
<div className="eta-card-icon">
|
<div className="eta-card-icon">
|
||||||
<FontAwesomeIcon icon={faClock} />
|
<FontAwesomeIcon
|
||||||
|
icon={faClock}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="eta-card-body">
|
<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 && (
|
{computedArrival && (
|
||||||
<p className="eta-card-value">{computedArrival}</p>
|
<p className="eta-card-value">
|
||||||
)}
|
{
|
||||||
{order.eta?.eta_minutes && order.eta.eta_minutes > 0 && (
|
computedArrival
|
||||||
<p className="eta-card-sub">
|
}
|
||||||
~{order.eta.eta_minutes} min restantes
|
|
||||||
</p>
|
</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">
|
<p className="eta-card-sub">
|
||||||
Distance :{" "}
|
Distance :{" "}
|
||||||
{typeof order.eta.livreur_distance === "number"
|
{typeof order
|
||||||
? order.eta.livreur_distance.toFixed(1)
|
.eta
|
||||||
: order.eta.livreur_distance}{" "}
|
.livreur_distance ===
|
||||||
|
"number"
|
||||||
|
? order.eta.livreur_distance.toFixed(
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
: order.eta
|
||||||
|
.livreur_distance}{" "}
|
||||||
km
|
km
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -927,19 +992,53 @@ function SuiviLivraison() {
|
|||||||
/>{" "}
|
/>{" "}
|
||||||
Montant total
|
Montant total
|
||||||
</h4>
|
</h4>
|
||||||
{(order.referral_used ?? 0) > 0 && (
|
{(order.referral_used ??
|
||||||
<p style={{ margin: "0 0 2px", fontSize: "0.85rem", color: "var(--text-muted)" }}>
|
0) > 0 && (
|
||||||
Brut : {(order.total_prix ?? 0).toFixed(2)} €
|
<p
|
||||||
|
style={{
|
||||||
|
margin: "0 0 2px",
|
||||||
|
fontSize:
|
||||||
|
"0.85rem",
|
||||||
|
color: "var(--text-muted)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Brut :{" "}
|
||||||
|
{(
|
||||||
|
order.total_prix ??
|
||||||
|
0
|
||||||
|
).toFixed(2)}{" "}
|
||||||
|
€
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<p className="total-amount">
|
<p className="total-amount">
|
||||||
<strong>
|
<strong>
|
||||||
{Math.max(0, getTotalAmount(order) - (order.referral_used ?? 0)).toFixed(2)} €
|
{Math.max(
|
||||||
|
0,
|
||||||
|
getTotalAmount(
|
||||||
|
order,
|
||||||
|
) -
|
||||||
|
(order.referral_used ??
|
||||||
|
0),
|
||||||
|
).toFixed(2)}{" "}
|
||||||
|
€
|
||||||
</strong>
|
</strong>
|
||||||
</p>
|
</p>
|
||||||
{(order.referral_used ?? 0) > 0 && (
|
{(order.referral_used ??
|
||||||
<p style={{ margin: "4px 0 0", fontSize: "0.82rem", color: "#10b981", fontWeight: 500 }}>
|
0) > 0 && (
|
||||||
— dont {(order.referral_used!).toFixed(2)} € parrainage déduit
|
<p
|
||||||
|
style={{
|
||||||
|
margin: "4px 0 0",
|
||||||
|
fontSize:
|
||||||
|
"0.82rem",
|
||||||
|
color: "#10b981",
|
||||||
|
fontWeight: 500,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
— dont{" "}
|
||||||
|
{order.referral_used!.toFixed(
|
||||||
|
2,
|
||||||
|
)}{" "}
|
||||||
|
€ parrainage déduit
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
formatOrderDate,
|
formatOrderDate,
|
||||||
formatPrice,
|
formatPrice,
|
||||||
calculateOrderTotal,
|
calculateOrderTotal,
|
||||||
|
getPublicSettings,
|
||||||
} from "../../api/api";
|
} from "../../api/api";
|
||||||
import type {
|
import type {
|
||||||
OrderDetail,
|
OrderDetail,
|
||||||
@@ -72,6 +73,7 @@ export default function OrderTrackingScreen() {
|
|||||||
const [penaltyWarning, setPenaltyWarning] =
|
const [penaltyWarning, setPenaltyWarning] =
|
||||||
useState<CancelCommandResponse | null>(null);
|
useState<CancelCommandResponse | null>(null);
|
||||||
const [penaltyOrderId, setPenaltyOrderId] = useState<number | null>(null);
|
const [penaltyOrderId, setPenaltyOrderId] = useState<number | null>(null);
|
||||||
|
const [penaltiesEnabled, setPenaltiesEnabled] = useState(false);
|
||||||
const [toastMsg, setToastMsg] = useState("");
|
const [toastMsg, setToastMsg] = useState("");
|
||||||
const [toastType, setToastType] = useState<
|
const [toastType, setToastType] = useState<
|
||||||
"success" | "error" | "warning" | "info"
|
"success" | "error" | "warning" | "info"
|
||||||
@@ -98,6 +100,12 @@ export default function OrderTrackingScreen() {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getPublicSettings()
|
||||||
|
.then((s) => setPenaltiesEnabled(s.penalties_enabled))
|
||||||
|
.catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -140,7 +148,6 @@ export default function OrderTrackingScreen() {
|
|||||||
"success",
|
"success",
|
||||||
);
|
);
|
||||||
fetchOrders();
|
fetchOrders();
|
||||||
navigation.navigate("OrderDetails", { orderId });
|
|
||||||
} else {
|
} else {
|
||||||
showToast(res.message || "Erreur", "error");
|
showToast(res.message || "Erreur", "error");
|
||||||
}
|
}
|
||||||
@@ -274,21 +281,6 @@ export default function OrderTrackingScreen() {
|
|||||||
gap: spacing.s,
|
gap: spacing.s,
|
||||||
marginTop: spacing.l,
|
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: {
|
proposalBox: {
|
||||||
backgroundColor: colors.warning + "18",
|
backgroundColor: colors.warning + "18",
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
@@ -402,7 +394,10 @@ export default function OrderTrackingScreen() {
|
|||||||
renderItem={({ item: order }) => {
|
renderItem={({ item: order }) => {
|
||||||
const expanded = expandedId === order.id;
|
const expanded = expandedId === order.id;
|
||||||
const gross = calculateOrderTotal(order);
|
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 progress = STATUS_PROGRESS[order.status] || 0;
|
||||||
const track = tracking[order.id];
|
const track = tracking[order.id];
|
||||||
const eta = etas[order.id];
|
const eta = etas[order.id];
|
||||||
@@ -466,8 +461,18 @@ export default function OrderTrackingScreen() {
|
|||||||
{formatPrice(total)}
|
{formatPrice(total)}
|
||||||
</Text>
|
</Text>
|
||||||
{(order.referral_used ?? 0) > 0 && (
|
{(order.referral_used ?? 0) > 0 && (
|
||||||
<Text style={{ fontSize: 11, color: colors.success, marginTop: 2 }}>
|
<Text
|
||||||
dont -{(order.referral_used as number).toFixed(2)} € parrainage
|
style={{
|
||||||
|
fontSize: 11,
|
||||||
|
color: colors.success,
|
||||||
|
marginTop: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
dont -
|
||||||
|
{(
|
||||||
|
order.referral_used as number
|
||||||
|
).toFixed(2)}{" "}
|
||||||
|
€ parrainage
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
@@ -481,16 +486,6 @@ export default function OrderTrackingScreen() {
|
|||||||
color={colors.textMuted}
|
color={colors.textMuted}
|
||||||
/>
|
/>
|
||||||
</View>
|
</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.address_proposal_status === "pending" &&
|
||||||
order.proposed_address && (
|
order.proposed_address && (
|
||||||
<View style={styles.proposalBox}>
|
<View style={styles.proposalBox}>
|
||||||
@@ -571,9 +566,7 @@ export default function OrderTrackingScreen() {
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
{order.status === "en_route" &&
|
{order.status === "en_route" && (
|
||||||
eta?.eta_minutes != null &&
|
|
||||||
eta.eta_minutes > 0 && (
|
|
||||||
<View style={styles.trackRow}>
|
<View style={styles.trackRow}>
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name="timer-outline"
|
name="timer-outline"
|
||||||
@@ -581,7 +574,10 @@ export default function OrderTrackingScreen() {
|
|||||||
color={colors.warning}
|
color={colors.warning}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.trackText}>
|
<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>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
@@ -593,7 +589,8 @@ export default function OrderTrackingScreen() {
|
|||||||
color={colors.warning}
|
color={colors.warning}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.trackText}>
|
<Text style={styles.trackText}>
|
||||||
Temps de livraison estimé : ~
|
Temps de livraison estimé :
|
||||||
|
~
|
||||||
{eta?.eta_minutes != null &&
|
{eta?.eta_minutes != null &&
|
||||||
eta.eta_minutes > 0 &&
|
eta.eta_minutes > 0 &&
|
||||||
eta.eta_minutes < 5
|
eta.eta_minutes < 5
|
||||||
@@ -627,6 +624,18 @@ export default function OrderTrackingScreen() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
|
{canConfirm && (
|
||||||
|
<Button
|
||||||
|
title="Confirmer reception"
|
||||||
|
onPress={() =>
|
||||||
|
setConfirmingId(
|
||||||
|
order.id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
variant="success"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{canCancel && (
|
{canCancel && (
|
||||||
<Button
|
<Button
|
||||||
title="Annuler"
|
title="Annuler"
|
||||||
@@ -691,6 +700,39 @@ export default function OrderTrackingScreen() {
|
|||||||
iconColor={colors.danger}
|
iconColor={colors.danger}
|
||||||
>
|
>
|
||||||
<View style={styles.modalBody}>
|
<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}>
|
<Text style={styles.modalText}>
|
||||||
Raison de l'annulation :
|
Raison de l'annulation :
|
||||||
</Text>
|
</Text>
|
||||||
@@ -753,12 +795,17 @@ export default function OrderTrackingScreen() {
|
|||||||
<Ionicons
|
<Ionicons
|
||||||
name="remove-circle-outline"
|
name="remove-circle-outline"
|
||||||
size={16}
|
size={16}
|
||||||
color={colors.warning}
|
color={colors.danger}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.penaltyDetail}>
|
<Text
|
||||||
Penalite:{" "}
|
style={[
|
||||||
|
styles.penaltyDetail,
|
||||||
|
{ color: colors.danger, fontWeight: "700" },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
Amende :{" "}
|
||||||
{penaltyWarning.penalty_warning.penalty_amount}{" "}
|
{penaltyWarning.penalty_warning.penalty_amount}{" "}
|
||||||
points
|
€
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user