// ============================================ // handlers/delivery_handlers.go // 🔧 VERSION MODIFIÉE avec ETA automatique // ============================================ package handlers import ( "encoding/json" "fmt" "gestion/db" "log" "math" "net/http" "strconv" "github.com/gin-gonic/gin" ) // ============================================ // 🔧 GetMyDeliveries // ============================================ func GetMyDeliveries(c *gin.Context) { database := c.MustGet("database").(*db.Database) username, exists := c.Get("username") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"}) return } if c.GetString("role") != "livreur" { c.JSON(http.StatusForbidden, gin.H{"error": "Accès réservé aux livreurs"}) return } usernameStr := username.(string) status := c.Query("status") commands, err := database.GetDeliveryPersonCommands(usernameStr, status) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": "Erreur récupération", "details": err.Error(), }) return } // ✨ FILTRAGE (SANS TÉLÉPHONE) filteredCommands := make([]gin.H, len(commands)) for i, cmd := range commands { commandID, _ := cmd["id"].(int) items, _ := database.GetCommandItems(commandID) // Client info SANS téléphone clientUsername, _ := cmd["username"].(string) client, _ := database.GetClientByUsername(clientUsername) clientInfo := gin.H{"nom": "Client", "prenom": ""} if client != nil { clientInfo = gin.H{ "nom": client.Nom, "prenom": client.Prenom, } } itemsSummary := make([]gin.H, len(items)) for j, item := range items { itemsSummary[j] = gin.H{ "produit": item["produit"], "quantite": item["quantite"], "prix": item["prix"], } } etaData, _ := database.GetCommandETA(commandID) filteredCommands[i] = gin.H{ "id": cmd["id"], "status": cmd["status"], "adresse": cmd["adresse"], "total_prix": cmd["total_prix"], "created_at": cmd["created_at"], "client_info": clientInfo, "items": itemsSummary, "items_count": len(items), "eta": etaData, } } log.Printf("✅ [MY_DELIVERIES] %d livraisons (données filtrées)", len(filteredCommands)) c.JSON(http.StatusOK, gin.H{ "success": true, "deliveries": filteredCommands, "count": len(filteredCommands), }) } // ============================================ // GetDeliveryDetails // ============================================ func GetDeliveryDetails(c *gin.Context) { database := c.MustGet("database").(*db.Database) username := c.GetString("username") if c.GetString("role") != "livreur" { c.JSON(http.StatusForbidden, gin.H{"error": "Accès réservé aux livreurs"}) return } commandID, err := strconv.Atoi(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "ID invalide"}) return } command, err := database.GetCommandByID(commandID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Commande non trouvée"}) return } // ✅ VÉRIFIER PROPRIÉTÉ livreurAssign, _ := command["livreur_assign"].(string) if livreurAssign != username { log.Printf("❌ Accès refusé - cmd assignée à %s", livreurAssign) c.JSON(http.StatusForbidden, gin.H{ "error": "Cette livraison ne vous est pas assignée", }) return } items, _ := database.GetCommandItems(commandID) clientUsername, _ := command["username"].(string) client, _ := database.GetClientByUsername(clientUsername) clientInfo := gin.H{"nom": "Client", "prenom": ""} if client != nil { clientInfo = gin.H{ "nom": client.Nom, "prenom": client.Prenom, } } itemsSummary := make([]gin.H, len(items)) for i, item := range items { itemsSummary[i] = gin.H{ "produit": item["produit"], "quantite": item["quantite"], "prix": item["prix"], } } etaData, _ := database.GetCommandETA(commandID) c.JSON(http.StatusOK, gin.H{ "success": true, "delivery": gin.H{ "id": command["id"], "status": command["status"], "adresse": command["adresse"], "total_prix": command["total_prix"], "created_at": command["created_at"], "client_info": clientInfo, "items": itemsSummary, "items_count": len(items), "eta": etaData, }, }) } // ============================================ // 🔧 UpdateDeliveryStatus - VERSION MODIFIÉE // ✅ CALCUL AUTOMATIQUE ETA lors du passage en "en_route" // ============================================ func UpdateDeliveryStatus(c *gin.Context) { database := c.MustGet("database").(*db.Database) username, exists := c.Get("username") if !exists || c.GetString("role") != "livreur" { c.JSON(http.StatusForbidden, gin.H{"error": "Accès réservé aux livreurs"}) return } usernameStr := username.(string) commandID, err := strconv.Atoi(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "ID invalide"}) return } var req struct { Status string `json:"status" binding:"required"` Notes string `json:"notes"` Latitude float64 `json:"latitude"` Longitude float64 `json:"longitude"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": "Données invalides", "details": err.Error(), }) return } log.Printf("📝 [UPD_STATUS] %s update cmd %d: %s", usernameStr, commandID, req.Status) command, err := database.GetCommandByID(commandID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Commande non trouvée"}) return } // ✅ VÉRIFIER PROPRIÉTÉ livreurAssign, _ := command["livreur_assign"].(string) if livreurAssign != usernameStr { log.Printf("❌ Accès refusé - assigné à %s", livreurAssign) c.JSON(http.StatusForbidden, gin.H{ "error": "Cette commande ne vous est pas assignée", }) return } // ✅ STATUTS VALIDES POUR LIVREUR (correspondant à la DB) validStatuses := []string{ "support", // Prise en charge "assigned", // Assigné (si auto-assignation) "en_route", // En route vers le client "arrived", // Arrivé à destination "livre", // Livré (en attente confirmation client) "failed", // Échec de livraison "cancelled", // Annulée } isValid := false for _, s := range validStatuses { if req.Status == s { isValid = true break } } if !isValid { c.JSON(http.StatusBadRequest, gin.H{ "error": "Statut invalide", "valid_statuses": validStatuses, "received": req.Status, }) return } // ✅ VALIDATION GPS pour livraison finale (livre ou failed) if (req.Status == "livre" || req.Status == "failed") && req.Latitude != 0 && req.Longitude != 0 { destLat, _ := command["dest_latitude"].(float64) destLon, _ := command["dest_longitude"].(float64) if destLat != 0 && destLon != 0 { distance := calculateDistance(req.Latitude, req.Longitude, destLat, destLon) log.Printf("📍 [GPS] Distance: %.2f m", distance) if distance > 100 { c.JSON(http.StatusBadRequest, gin.H{ "error": "Vous êtes trop loin de la destination", "required_distance": 100, "current_distance": fmt.Sprintf("%.2f", distance), "unit": "meters", }) return } log.Printf("✅ [GPS] Validation OK") } else { log.Printf("⚠️ [GPS] Coordonnées de destination non disponibles, validation ignorée") } } // Mettre à jour le statut if err := database.UpdateCommandStatus(commandID, req.Status); err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": "Erreur mise à jour", "details": err.Error(), }) return } // ✅ SI PASSAGE EN "EN_ROUTE" → CALCULER ET DÉFINIR L'ETA var etaMinutes int var etaMessage string if req.Status == "en_route" { log.Printf("🚗 [STATUS_LIVREUR] Passage en 'en_route' - Calcul ETA...") // Récupérer les coordonnées destination var destLat, destLon float64 // 1. Essayer le cache Redis destCacheKey := fmt.Sprintf("command:destination:%d", commandID) destData, err := db.Redis.Get(db.RedisCtx, destCacheKey).Result() if err == nil && destData != "" { var coords struct { Lat float64 `json:"lat"` Lon float64 `json:"lon"` } if err := json.Unmarshal([]byte(destData), &coords); err == nil && coords.Lat != 0 && coords.Lon != 0 { destLat = coords.Lat destLon = coords.Lon log.Printf("📍 [STATUS_LIVREUR] Coords depuis cache Redis: (%.6f, %.6f)", destLat, destLon) } } // 2. Fallback: récupérer depuis la DB if destLat == 0 || destLon == 0 { if dLat, ok := command["dest_latitude"].(float64); ok && dLat != 0 { destLat = dLat } if dLon, ok := command["dest_longitude"].(float64); ok && dLon != 0 { destLon = dLon } if destLat != 0 && destLon != 0 { log.Printf("📍 [STATUS_LIVREUR] Coords depuis DB: (%.6f, %.6f)", destLat, destLon) } } // 3. Calculer l'ETA depuis la position du livreur if destLat != 0 && destLon != 0 { etaMinutes = database.CalculateETAForDeliveryman(usernameStr, destLat, destLon) // Définir l'ETA dans Redis if err := database.SetCommandETA(commandID, etaMinutes); err != nil { log.Printf("⚠️ [STATUS_LIVREUR] Erreur définition ETA: %v", err) } else { log.Printf("✅ [STATUS_LIVREUR] ETA défini: %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 // Fallback database.SetCommandETA(commandID, etaMinutes) etaMessage = "Arrivée prévue dans 30 minutes (estimation par défaut)" } // Mettre à jour le statut du livreur en "delivering" database.SetDeliveryPersonStatus(usernameStr, "delivering", commandID) log.Printf("🚗 [STATUS_LIVREUR] Statut livreur mis à jour: delivering") } // Log message := req.Notes if message == "" { message = getDeliveryStatusMessage(req.Status) } if etaMessage != "" { message += fmt.Sprintf(" - %s", etaMessage) } database.AddCommandLog(commandID, req.Status, message, usernameStr) // ✅ NOTIFICATION CLIENT clientUsername, _ := command["username"].(string) if clientUsername != "" { var clientMsg string switch req.Status { case "support": clientMsg = fmt.Sprintf("Votre commande #%d est prise en charge", commandID) case "en_route": if etaMinutes > 0 { clientMsg = fmt.Sprintf("Votre commande #%d est en route ! Arrivée dans ~%d min", commandID, etaMinutes) } else { clientMsg = fmt.Sprintf("Votre commande #%d est en route !", commandID) } case "arrived": clientMsg = fmt.Sprintf("🛵 Votre livreur est là ! Il sera chez vous dans 5 minutes (commande #%d)", commandID) case "livre": clientMsg = fmt.Sprintf("Votre commande #%d a été livrée. Merci !", commandID) case "failed": clientMsg = fmt.Sprintf("Échec de livraison pour la commande #%d", commandID) } if clientMsg != "" { database.NotifyClient(clientUsername, commandID, req.Status, clientMsg) } } // ✅ GESTION SPÉCIALE SELON LE STATUT switch req.Status { case "livre": // Livraison terminée - Optimiser la queue log.Printf("📦 Livraison marquée 'livre' - Optimisation queue...") database.CompleteDeliveryAndProcessNext(usernameStr, commandID) case "failed": // Échec de livraison - Optimiser la queue log.Printf("❌ Livraison échouée - Optimisation queue...") database.CompleteDeliveryAndProcessNext(usernameStr, commandID) // Créer un problème de livraison database.CreateDeliveryIssue( commandID, "delivery_failed", fmt.Sprintf("Échec de livraison: %s", req.Notes), usernameStr, ) case "arrived": log.Printf("📍 Livreur arrivé à destination - Commande %d", commandID) } response := gin.H{ "success": true, "message": "Statut mis à jour", "command_id": commandID, "status": req.Status, } if req.Status == "en_route" && etaMinutes > 0 { response["eta_minutes"] = etaMinutes response["eta_message"] = etaMessage } c.JSON(http.StatusOK, response) } // ============================================ // HELPERS // ============================================ func calculateDistance(lat1, lon1, lat2, lon2 float64) float64 { const earthRadiusKm = 6371 const metersPerKm = 1000 lat1Rad := degreesToRadians(lat1) lon1Rad := degreesToRadians(lon1) lat2Rad := degreesToRadians(lat2) lon2Rad := degreesToRadians(lon2) dLat := lat2Rad - lat1Rad dLon := lon2Rad - lon1Rad a := math.Sin(dLat/2)*math.Sin(dLat/2) + math.Cos(lat1Rad)*math.Cos(lat2Rad)* math.Sin(dLon/2)*math.Sin(dLon/2) c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a)) return earthRadiusKm * c * metersPerKm } func degreesToRadians(degrees float64) float64 { return degrees * math.Pi / 180 } func getDeliveryStatusMessage(status string) string { messages := map[string]string{ "support": "Prise en charge de la livraison", "assigned": "Commande assignée", "en_route": "En route vers le client", "arrived": "Arrivé à destination", "livre": "Livraison effectuée", "failed": "Échec de livraison", "cancelled": "Livraison annulée", } if msg, ok := messages[status]; ok { return msg } return fmt.Sprintf("Statut changé: %s", status) }