Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ac824be81 | ||
|
|
28b9491338 |
@@ -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 {
|
||||
|
||||
@@ -3,8 +3,10 @@ package handlers
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"gestion/db"
|
||||
"gestion/services"
|
||||
"gestion/utils"
|
||||
"log"
|
||||
"net/http"
|
||||
@@ -630,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")
|
||||
@@ -679,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{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
DB_HOST=postgres
|
||||
DB_PORT=5432
|
||||
DB_USER=postgres
|
||||
DB_PASSWORD=Ia3JWjw3Y0HzlEXH6QH3pqEu09Fap5C420
|
||||
DB_PASSWORD=1SWDxH20rV7K2Uc2PNlwCaCxfVZEtKomF0CK9OMh
|
||||
DB_NAME=gestion_db
|
||||
DB_SSLMODE=disable
|
||||
SESSION_SECRET=GwDgqYn7Tn4x6Hs9ZjUD6HP8B7pQWK
|
||||
@@ -9,7 +9,7 @@ USER_JWT_SECRET=69F5ujM1YZ6JBh3pXczc3j0JzBuAvU
|
||||
ADMIN_JWT_SECRET=RwPxdzSzAR7HcrufA6kEXHFdIiEX87
|
||||
REDIS_HOST=redis
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=m3hQyr4BgF0Paer1H4a5iUnzXqjUji
|
||||
REDIS_PASSWORD=k6UYX9RtuXJVV1HUeefbSukMcSwjvVgRsh2qJGPh
|
||||
TOMTOM_API_KEY=MERY8I7LMeYVSLKO5WuV73W9rKJpBLoB
|
||||
TOMTOM_API_KEY_1=6F7HHk8GT6WGlZ22W4gfAbRiQk5lJoGV
|
||||
TELEGRAM_BOT_TOKEN=7419967935:AAEeNIzlK6DqcQTL8q63zQ-Ted5W5VOd-LI
|
||||
|
||||
@@ -39,12 +39,6 @@ services:
|
||||
- backend_uploads:/app/uploads
|
||||
networks:
|
||||
- gestion-network
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
|
||||
# =========================================================
|
||||
# Frontend Web (React/Vite — servi en HTTP interne)
|
||||
# =========================================================
|
||||
@@ -83,66 +77,6 @@ services:
|
||||
- backend
|
||||
- frontend
|
||||
|
||||
# =========================================================
|
||||
# PostgreSQL
|
||||
# =========================================================
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: gestion-postgres
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- POSTGRES_USER=${DB_USER:-postgres}
|
||||
- POSTGRES_PASSWORD=${DB_PASSWORD}
|
||||
- POSTGRES_DB=${DB_NAME:-gestion_db}
|
||||
- PGDATA=/var/lib/postgresql/data/pgdata
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
networks:
|
||||
- gestion-network
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD-SHELL",
|
||||
"pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-gestion_db}",
|
||||
]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 10s
|
||||
|
||||
# =========================================================
|
||||
# Redis
|
||||
# =========================================================
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: gestion-redis
|
||||
restart: unless-stopped
|
||||
command: >
|
||||
redis-server
|
||||
--requirepass ${REDIS_PASSWORD}
|
||||
--appendonly yes
|
||||
--appendfsync everysec
|
||||
--maxmemory 256mb
|
||||
--maxmemory-policy allkeys-lru
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
networks:
|
||||
- gestion-network
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"redis-cli",
|
||||
"--no-auth-warning",
|
||||
"-a",
|
||||
"${REDIS_PASSWORD}",
|
||||
"ping",
|
||||
]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
start_period: 10s
|
||||
|
||||
# =========================================================
|
||||
# LBTelegram — Gateway Telegram load balancer
|
||||
# =========================================================
|
||||
|
||||
@@ -675,10 +675,12 @@ 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}`}
|
||||
{computedArrival
|
||||
? ` Vers ${computedArrival}`
|
||||
: " Aucune heure disponible"}
|
||||
</div>
|
||||
)}
|
||||
{showArrivedEta && (
|
||||
|
||||
@@ -551,9 +551,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"
|
||||
@@ -561,7 +559,9 @@ 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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user