package handlers import ( "encoding/json" "fmt" "gestion/db" "gestion/services" "log" "net/http" "strconv" "time" "github.com/gin-gonic/gin" ) func GeocodeAddress(c *gin.Context) { geoService := c.MustGet("geoService").(*services.GeoService) var req struct { Address string `json:"address" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Adresse requise"}) return } location, err := geoService.GeocodeAddress(req.Address) if err != nil { // Tentative de correction — resolveAddress ne touche pas à c.JSON suggestion, err := resolveAddress(geoService, req.Address) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Adresse introuvable, vérifiez l'orthographe"}) return } log.Printf("✅ Adresse corrigée: '%s' → '%s' (confiance %.0f%%)", req.Address, suggestion.CorrectedAddress, suggestion.Confidence*100) c.JSON(http.StatusOK, gin.H{ "success": true, "latitude": suggestion.Coordinates.Latitude, "longitude": suggestion.Coordinates.Longitude, "display_name": suggestion.CorrectedAddress, "correction_applied": suggestion.CorrectionApplied, "confidence": suggestion.Confidence, }) return } log.Printf("📍 Adresse géocodée: %s -> (%.6f, %.6f)", req.Address, location.Latitude, location.Longitude) c.JSON(http.StatusOK, gin.H{ "success": true, "latitude": location.Latitude, "longitude": location.Longitude, "display_name": location.DisplayName, "correction_applied": false, }) } // resolveAddress : logique pure, sans toucher à gin.Context func resolveAddress(geoService *services.GeoService, address string) (*services.AddressSuggestion, error) { return geoService.CorrectionService().ResolveAddress(address) } // FindNearestDeliveryPerson trouve le livreur le plus proche d'une adresse func FindNearestDeliveryPerson(c *gin.Context) { database := c.MustGet("database").(*db.Database) geoService := c.MustGet("geoService").(*services.GeoService) userRole := c.GetString("role") if userRole != "admin" { c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"}) return } var req struct { Address string `json:"address"` 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", }) return } var targetCoords services.Coordinates // Si adresse fournie, la géocoder if req.Address != "" { location, err := geoService.GeocodeAddress(req.Address) if err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": "Impossible de géocoder l'adresse", }) return } targetCoords.Latitude = location.Latitude targetCoords.Longitude = location.Longitude } else if req.Latitude != 0 && req.Longitude != 0 { // Sinon utiliser les coordonnées fournies targetCoords.Latitude = req.Latitude targetCoords.Longitude = req.Longitude } else { c.JSON(http.StatusBadRequest, gin.H{ "error": "Fournir soit une adresse, soit des coordonnées GPS", }) return } // Valider les coordonnées if err := services.ValidateCoordinates(targetCoords); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": "Coordonnées invalides", }) return } // Récupérer les livreurs disponibles availableLivreurs, err := database.GetAvailableDeliveryPersonsRedis() if err != nil || len(availableLivreurs) == 0 { c.JSON(http.StatusNotFound, gin.H{ "error": "Aucun livreur disponible", }) return } // Extraire les usernames usernames := make([]string, len(availableLivreurs)) for i, livreur := range availableLivreurs { usernames[i] = livreur.Username } // Trouver le plus proche (calcul rapide avec Haversine) nearest, err := geoService.FindNearestDeliveryPersonFast(targetCoords, usernames) if err != nil { c.JSON(http.StatusNotFound, gin.H{ "error": "Aucun livreur avec position GPS valide", }) return } // Recalculer l'ETA du plus proche avec TomTom pour plus de précision etaWithTraffic, distanceReal, err := services.GetETAWithTraffic(nearest.Location, targetCoords) if err == nil { nearest.EstimatedTime = etaWithTraffic nearest.Distance = distanceReal log.Printf("✅ Livreur le plus proche: %s (%.2f km, ~%d min avec trafic)", nearest.Username, nearest.Distance, nearest.EstimatedTime) } else { log.Printf("✅ Livreur le plus proche: %s (%.2f km, ~%d min sans trafic)", nearest.Username, nearest.Distance, nearest.EstimatedTime) } c.JSON(http.StatusOK, gin.H{ "success": true, "target": gin.H{ "latitude": targetCoords.Latitude, "longitude": targetCoords.Longitude, }, "nearest_delivery_person": gin.H{ "username": nearest.Username, "latitude": nearest.Location.Latitude, "longitude": nearest.Location.Longitude, "distance_km": nearest.Distance, "eta_minutes": nearest.EstimatedTime, "traffic_aware": err == nil, }, }) } // GetAllDeliveryDistances retourne tous les livreurs triés par distance func GetAllDeliveryDistances(c *gin.Context) { database := c.MustGet("database").(*db.Database) geoService := c.MustGet("geoService").(*services.GeoService) userRole := c.GetString("role") if userRole != "admin" { c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"}) return } var req struct { Address string `json:"address"` 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", }) return } var targetCoords services.Coordinates if req.Address != "" { location, err := geoService.GeocodeAddress(req.Address) if err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": "Impossible de géocoder l'adresse", }) return } targetCoords.Latitude = location.Latitude targetCoords.Longitude = location.Longitude } else if req.Latitude != 0 && req.Longitude != 0 { targetCoords.Latitude = req.Latitude targetCoords.Longitude = req.Longitude } else { c.JSON(http.StatusBadRequest, gin.H{ "error": "Fournir soit une adresse, soit des coordonnées GPS", }) return } if err := services.ValidateCoordinates(targetCoords); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": "Coordonnées invalides", }) return } availableLivreurs, err := database.GetAvailableDeliveryPersonsRedis() if err != nil || len(availableLivreurs) == 0 { c.JSON(http.StatusNotFound, gin.H{ "error": "Aucun livreur disponible", }) return } usernames := make([]string, len(availableLivreurs)) for i, livreur := range availableLivreurs { usernames[i] = livreur.Username } distances, err := geoService.GetAllDeliveryDistances(targetCoords, usernames) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": "Erreur calcul des distances", }) return } c.JSON(http.StatusOK, gin.H{ "success": true, "target": gin.H{ "latitude": targetCoords.Latitude, "longitude": targetCoords.Longitude, }, "delivery_persons": distances, "count": len(distances), }) } func AutoAssignNearestDeliveryPerson(c *gin.Context) { database := c.MustGet("database").(*db.Database) geoService := c.MustGet("geoService").(*services.GeoService) userRole := c.GetString("role") if userRole != "admin" { c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"}) return } commandID, err := strconv.Atoi(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "ID de commande invalide"}) return } // Récupérer la commande command, err := database.GetCommandByID(commandID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Commande non trouvée"}) return } status, ok := command["status"].(string) if !ok || (status != "pending" && status != "priority") { c.JSON(http.StatusBadRequest, gin.H{ "error": "La commande doit être en statut 'pending' ou 'priority'", "current_status": status, }) return } // Récupérer l'adresse de livraison de la commande address, ok := command["adresse"].(string) if !ok || address == "" || address == "Adresse non spécifiée" { c.JSON(http.StatusBadRequest, gin.H{ "error": "Cette commande n'a pas d'adresse de livraison valide", "command_id": commandID, "message": "L'adresse de livraison doit être définie lors de la création de la commande", }) return } log.Printf("📍 Adresse de livraison: %s", address) // Géocoder l'adresse location, err := geoService.GeocodeAddress(address) if err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": "Impossible de géocoder l'adresse de livraison", "address": address, }) return } log.Printf("📍 Adresse géocodée: %s -> (%.6f, %.6f)", address, location.Latitude, location.Longitude) destCacheKey := fmt.Sprintf("command:destination:%d", commandID) coordsJSON, _ := json.Marshal(map[string]float64{ "lat": location.Latitude, "lon": location.Longitude, }) if err := db.Redis.Set(db.RedisCtx, destCacheKey, coordsJSON, 4*time.Hour).Err(); err != nil { log.Printf("⚠️ Impossible de sauvegarder coordonnées destination: %v", err) } else { log.Printf("📍 Coordonnées destination sauvegardées pour commande %d: (%.6f, %.6f)", commandID, location.Latitude, location.Longitude) } targetCoords := services.Coordinates{ Latitude: location.Latitude, Longitude: location.Longitude, } // Compter le nombre de livreurs actifs activeCount, _ := database.CountActiveDeliverymen() if activeCount == 0 { c.JSON(http.StatusNotFound, gin.H{ "error": "Aucun livreur actif (tous sont offline)", }) return } log.Printf("🚗 %d livreur(s) actif(s)", activeCount) activeLivreurs, err := database.GetAllActiveDeliveryPersons() if err != nil || len(activeLivreurs) == 0 { if activeCount == 1 { singleDeliveryman, err := database.GetSingleActiveDeliveryman() if err != nil { c.JSON(http.StatusNotFound, gin.H{ "error": "Impossible de trouver le livreur actif", }) return } // Calculer ETA avec TomTom travelTime, distance, err := calculateTravelTimeWithTomTom(geoService, singleDeliveryman, location.Latitude, location.Longitude) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": "Erreur calcul ETA", }) return } err = database.AssignCommandToDeliverymanQueueWithCoords(commandID, singleDeliveryman, travelTime, location.Latitude, location.Longitude, address) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": "Erreur lors de l'assignation", }) return } database.SetDeliveryPersonStatus(singleDeliveryman, "busy", commandID) queueInfo, _ := database.GetDeliverymanQueueInfo(singleDeliveryman) etaData, _ := database.GetCommandETA(commandID) log.Printf("✅ Commande %d assignée au seul livreur actif %s (%.2f km)", commandID, singleDeliveryman, distance) c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Commande assignée au seul livreur actif (sans limite)", "command_id": commandID, "assigned_to": gin.H{ "username": singleDeliveryman, "travel_time": travelTime, "distance_km": distance, "queue_position": queueInfo["queue_size"], "single_driver": true, "traffic_aware": true, }, "eta": etaData, "delivery_address": address, "coordinates": gin.H{ "latitude": location.Latitude, "longitude": location.Longitude, }, "queue_info": queueInfo, }) return } allAtCapacity, numActive, _ := database.AreAllDeliverymenAtCapacity() if allAtCapacity && numActive > 1 { log.Printf("⚠️ Tous les %d livreurs sont à capacité max - Distribution forcée", numActive) leastLoaded, currentSize, err := database.GetLeastLoadedDeliverymanForced() if err != nil { c.JSON(http.StatusNotFound, gin.H{ "error": "Impossible de trouver un livreur pour la distribution forcée", }) return } travelTime, distance, err := calculateTravelTimeWithTomTom(geoService, leastLoaded, location.Latitude, location.Longitude) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": "Erreur calcul ETA", }) return } err = database.ForceAssignCommandToDeliverymanWithCoords(commandID, leastLoaded, travelTime, location.Latitude, location.Longitude, address) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": "Erreur lors de l'assignation forcée", }) return } database.SetDeliveryPersonStatus(leastLoaded, "busy", commandID) queueInfo, _ := database.GetDeliverymanQueueInfo(leastLoaded) etaData, _ := database.GetCommandETA(commandID) log.Printf("✅ FORCE: Commande %d assignée à %s (capacité dépassée: %d, %.2f km)", commandID, leastLoaded, currentSize+1, distance) c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Commande assignée par distribution forcée (capacité max dépassée)", "command_id": commandID, "forced": true, "assigned_to": gin.H{ "username": leastLoaded, "travel_time": travelTime, "distance_km": distance, "queue_position": currentSize + 1, "over_capacity": true, "traffic_aware": true, }, "eta": etaData, "delivery_address": address, "coordinates": gin.H{ "latitude": location.Latitude, "longitude": location.Longitude, }, "queue_info": queueInfo, }) return } c.JSON(http.StatusNotFound, gin.H{ "error": "Aucun livreur actif avec capacité disponible", "active_count": activeCount, "max_per_deliveryman": db.MAX_COMMANDS_PER_DELIVERYMAN, }) return } usernames := make([]string, len(activeLivreurs)) for i, livreur := range activeLivreurs { usernames[i] = livreur.Username } nearest, err := geoService.FindNearestDeliveryPersonFast(targetCoords, usernames) if err != nil { c.JSON(http.StatusNotFound, gin.H{ "error": "Aucun livreur avec position GPS valide", }) return } travelTime, distance, err := services.GetETAWithTraffic(nearest.Location, targetCoords) if err != nil { // Fallback sur le calcul initial travelTime = nearest.EstimatedTime distance = nearest.Distance log.Printf("⚠️ TomTom indisponible, utilisation du calcul Haversine") } log.Printf("🎯 Livreur le plus proche: %s (%.2f km, ~%d min)", nearest.Username, distance, travelTime) err = database.AssignCommandToDeliverymanQueueWithCoords(commandID, nearest.Username, travelTime, location.Latitude, location.Longitude, address) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": "Erreur lors de l'assignation", }) return } database.SetDeliveryPersonStatus(nearest.Username, "busy", commandID) queueInfo, _ := database.GetDeliverymanQueueInfo(nearest.Username) etaData, _ := database.GetCommandETA(commandID) log.Printf("✅ Commande %d assignée à la queue de %s", commandID, nearest.Username) c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Commande assignée à la queue du livreur", "command_id": commandID, "assigned_to": gin.H{ "username": nearest.Username, "distance_km": distance, "travel_time": travelTime, "queue_position": queueInfo["queue_size"], "single_driver": activeCount == 1, "traffic_aware": err == nil, }, "eta": etaData, "delivery_address": address, "coordinates": gin.H{ "latitude": location.Latitude, "longitude": location.Longitude, }, "queue_info": queueInfo, }) } func AutoAssignAllPendingCommands(c *gin.Context) { database := c.MustGet("database").(*db.Database) geoService := c.MustGet("geoService").(*services.GeoService) userRole := c.GetString("role") if userRole != "admin" { c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"}) return } commands, err := database.GetAllCommands("pending", "") if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": "Erreur récupération des commandes", }) return } if len(commands) == 0 { c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Aucune commande en attente", "assigned": 0, }) return } log.Printf("📋 %d commandes en attente à assigner", len(commands)) var assigned []gin.H var failed []gin.H for _, cmd := range commands { commandID, ok := cmd["id"].(int) if !ok { if idFloat, ok := cmd["id"].(float64); ok { commandID = int(idFloat) } else { continue } } address, ok := cmd["adresse"].(string) if !ok || address == "" || address == "Adresse non spécifiée" { failed = append(failed, gin.H{ "command_id": commandID, "error": "Adresse de livraison manquante", }) continue } location, err := geoService.GeocodeAddress(address) if err != nil { failed = append(failed, gin.H{ "command_id": commandID, "error": fmt.Sprintf("Impossible de géocoder: %s", address), }) continue } targetCoords := services.Coordinates{ Latitude: location.Latitude, Longitude: location.Longitude, } activeLivreurs, err := database.GetAllActiveDeliveryPersons() if err != nil || len(activeLivreurs) == 0 { failed = append(failed, gin.H{ "command_id": commandID, "error": "Aucun livreur disponible", }) continue } usernames := make([]string, len(activeLivreurs)) for i, livreur := range activeLivreurs { usernames[i] = livreur.Username } nearest, err := geoService.FindNearestDeliveryPersonFast(targetCoords, usernames) if err != nil { failed = append(failed, gin.H{ "command_id": commandID, "error": "Aucun livreur avec position GPS", }) continue } travelTime := nearest.EstimatedTime distance := nearest.Distance // Assigner à la queue err = database.AssignCommandToDeliverymanQueue(commandID, nearest.Username, travelTime) if err != nil { failed = append(failed, gin.H{ "command_id": commandID, "error": err.Error(), }) continue } database.SetDeliveryPersonStatus(nearest.Username, "busy", commandID) etaData, _ := database.GetCommandETA(commandID) var totalETA, waitTime any totalETA = "N/A" waitTime = "N/A" if etaData != nil { if val, exists := etaData["total_eta_minutes"]; exists { totalETA = val } if val, exists := etaData["wait_time_minutes"]; exists { waitTime = val } } assigned = append(assigned, gin.H{ "command_id": commandID, "assigned_to": nearest.Username, "distance_km": distance, "total_eta_minutes": totalETA, "wait_time_minutes": waitTime, "travel_time": travelTime, }) log.Printf("✅ Commande %d -> %s (ETA: %v min)", commandID, nearest.Username, totalETA) } queuesOverview, _ := database.GetAllQueuesOverview() c.JSON(http.StatusOK, gin.H{ "success": true, "message": fmt.Sprintf("%d commandes assignées, %d échecs", len(assigned), len(failed)), "total_pending": len(commands), "assigned_count": len(assigned), "failed_count": len(failed), "assigned": assigned, "failed": failed, "queues_overview": queuesOverview, "note": "ETAs calculés avec Haversine pour rapidité, précision TomTom disponible individuellement", }) } // GetAllDeliveryQueues retourne l'état de toutes les queues des livreurs func GetAllDeliveryQueues(c *gin.Context) { database := c.MustGet("database").(*db.Database) userRole := c.GetString("role") if userRole != "admin" { c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"}) return } overview, err := database.GetAllQueuesOverview() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": "Erreur récupération des queues", }) return } var deliverymenDetails []gin.H keys, _ := db.Redis.Keys(db.RedisCtx, "delivery:status:*").Result() for _, key := range keys { username := key[len("delivery:status:"):] queueInfo, _ := database.GetDeliverymanQueueInfo(username) // Récupérer le statut statusData, _ := db.Redis.Get(db.RedisCtx, key).Result() var status map[string]any if statusData != "" { json.Unmarshal([]byte(statusData), &status) } deliverymenDetails = append(deliverymenDetails, gin.H{ "username": username, "status": status["status"], "queue_info": queueInfo, }) } c.JSON(http.StatusOK, gin.H{ "success": true, "overview": overview, "deliverymen_detail": deliverymenDetails, }) } func GetDeliverymanQueue(c *gin.Context) { database := c.MustGet("database").(*db.Database) userRole := c.GetString("role") if userRole != "admin" { c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"}) return } username := c.Param("username") if username == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Username requis"}) return } queueInfo, err := database.GetDeliverymanQueueInfo(username) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": "Erreur récupération de la queue", }) return } c.JSON(http.StatusOK, gin.H{ "success": true, "queue_info": queueInfo, }) } func ValidateAddress(c *gin.Context) { geoService := c.MustGet("geoService").(*services.GeoService) var req struct { Address string `json:"address" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Adresse requise"}) return } if !geoService.IsValidAddress(req.Address) { c.JSON(http.StatusOK, gin.H{"valid": false, "message": "Adresse introuvable ou invalide"}) return } location, _ := geoService.GeocodeAddress(req.Address) c.JSON(http.StatusOK, gin.H{ "valid": true, "message": "Adresse valide", "latitude": location.Latitude, "longitude": location.Longitude, "display_name": location.DisplayName, }) } func calculateTravelTimeWithTomTom(geoService *services.GeoService, deliverymanUsername string, targetLat, targetLon float64) (int, float64, error) { deliverymanLoc, err := geoService.GetDeliveryPersonLocation(deliverymanUsername) if err != nil { return 0, 0, fmt.Errorf("position du livreur introuvable: %w", err) } targetCoords := services.Coordinates{ Latitude: targetLat, Longitude: targetLon, } travelTime, distance, err := services.GetETAWithTraffic(*deliverymanLoc, targetCoords) if err != nil { distance = services.CalculateDistance(*deliverymanLoc, targetCoords) travelTime = services.CalculateETA(distance) log.Printf("⚠️ TomTom indisponible pour %s, fallback: %.2f km -> %d min", deliverymanUsername, distance, travelTime) } else { log.Printf("🛣️ TomTom utilisé pour %s: %.2f km -> %d min (trafic réel)", deliverymanUsername, distance, travelTime) } return travelTime, distance, nil }