901 lines
26 KiB
Go
901 lines
26 KiB
Go
// ============================================
|
|
// handlers/geo_handlers.go - VERSION CORRIGÉE COMPLÈTE
|
|
// ============================================
|
|
|
|
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"gestion/db"
|
|
"gestion/services"
|
|
"log"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// ============================================
|
|
// GÉOCODAGE D'ADRESSES
|
|
// ============================================
|
|
|
|
// GeocodeAddress convertit une adresse en coordonnées GPS
|
|
// POST /api/v1/geocode
|
|
// Body: {"address": "1600 Amphitheatre Parkway, Mountain View, CA"}
|
|
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",
|
|
"details": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
location, err := geoService.GeocodeAddress(req.Address)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{
|
|
"error": "Impossible de géocoder cette adresse",
|
|
"details": err.Error(),
|
|
})
|
|
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,
|
|
})
|
|
}
|
|
|
|
// ============================================
|
|
// RECHERCHE DU LIVREUR LE PLUS PROCHE
|
|
// ============================================
|
|
|
|
// 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",
|
|
"details": err.Error(),
|
|
})
|
|
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",
|
|
"details": err.Error(),
|
|
})
|
|
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",
|
|
"details": err.Error(),
|
|
})
|
|
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",
|
|
"details": err.Error(),
|
|
})
|
|
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,
|
|
},
|
|
})
|
|
}
|
|
|
|
// ============================================
|
|
// LISTE TOUS LES LIVREURS TRIÉS PAR DISTANCE
|
|
// ============================================
|
|
|
|
// GetAllDeliveryDistances retourne tous les livreurs triés par distance
|
|
// POST /api/v2/admin/protected/delivery/distances
|
|
// Body: {"address": "123 Main St"} ou {"latitude": 48.8566, "longitude": 2.3522}
|
|
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",
|
|
"details": err.Error(),
|
|
})
|
|
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",
|
|
"details": err.Error(),
|
|
})
|
|
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",
|
|
"details": err.Error(),
|
|
})
|
|
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",
|
|
"details": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"target": gin.H{
|
|
"latitude": targetCoords.Latitude,
|
|
"longitude": targetCoords.Longitude,
|
|
},
|
|
"delivery_persons": distances,
|
|
"count": len(distances),
|
|
})
|
|
}
|
|
|
|
// ============================================
|
|
// AUTO-ASSIGNATION INTELLIGENTE AVEC QUEUE MULTI-COMMANDES
|
|
// ============================================
|
|
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,
|
|
"details": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
log.Printf("📍 Adresse géocodée: %s -> (%.6f, %.6f)", address, location.Latitude, location.Longitude)
|
|
|
|
// ============================================
|
|
// 🔹 SAUVEGARDER LES COORDONNÉES DANS LE CACHE REDIS
|
|
// ============================================
|
|
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)
|
|
|
|
// Récupérer les livreurs actifs avec capacité disponible
|
|
activeLivreurs, err := database.GetAllActiveDeliveryPersons()
|
|
|
|
// Si aucun livreur avec capacité disponible
|
|
if err != nil || len(activeLivreurs) == 0 {
|
|
// Cas 1: Un seul livreur actif -> pas de limite
|
|
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",
|
|
"details": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
// ✅ Passer les coordonnées à la fonction d'assignation
|
|
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",
|
|
"details": err.Error(),
|
|
})
|
|
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)
|
|
|
|
// ✅ CORRECTION: Utiliser etaData directement sans accès aux clés
|
|
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, // ✅ Directement l'objet complet
|
|
"delivery_address": address,
|
|
"coordinates": gin.H{
|
|
"latitude": location.Latitude,
|
|
"longitude": location.Longitude,
|
|
},
|
|
"queue_info": queueInfo,
|
|
})
|
|
return
|
|
}
|
|
|
|
// Cas 2: Plusieurs livreurs mais tous à capacité max -> Distribution forcée
|
|
allAtCapacity, numActive, _ := database.AreAllDeliverymenAtCapacity()
|
|
|
|
if allAtCapacity && numActive > 1 {
|
|
log.Printf("⚠️ Tous les %d livreurs sont à capacité max - Distribution forcée", numActive)
|
|
|
|
// Trouver le livreur le moins chargé (même s'il dépasse 10)
|
|
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
|
|
}
|
|
|
|
// Calculer le temps de trajet avec TomTom
|
|
travelTime, distance, err := calculateTravelTimeWithTomTom(geoService, leastLoaded, location.Latitude, location.Longitude)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"error": "Erreur calcul ETA",
|
|
"details": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
// ✅ Assigner de force avec coordonnées
|
|
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",
|
|
"details": err.Error(),
|
|
})
|
|
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)
|
|
|
|
// ✅ CORRECTION: Utiliser etaData directement
|
|
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, // ✅ Directement l'objet complet
|
|
"delivery_address": address,
|
|
"coordinates": gin.H{
|
|
"latitude": location.Latitude,
|
|
"longitude": location.Longitude,
|
|
},
|
|
"queue_info": queueInfo,
|
|
})
|
|
return
|
|
}
|
|
|
|
// Cas 3: Erreur générique
|
|
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
|
|
}
|
|
|
|
// Cas normal: Au moins un livreur avec capacité disponible
|
|
usernames := make([]string, len(activeLivreurs))
|
|
for i, livreur := range activeLivreurs {
|
|
usernames[i] = livreur.Username
|
|
}
|
|
|
|
// Trouver le livreur le plus proche (calcul rapide)
|
|
nearest, err := geoService.FindNearestDeliveryPersonFast(targetCoords, usernames)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{
|
|
"error": "Aucun livreur avec position GPS valide",
|
|
"details": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
// Recalculer l'ETA avec TomTom pour plus de précision
|
|
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)
|
|
|
|
// ✅ Assigner à la queue du livreur avec coordonnées
|
|
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",
|
|
"details": err.Error(),
|
|
})
|
|
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)
|
|
|
|
// ✅ CORRECTION: Utiliser etaData directement
|
|
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, // ✅ Directement l'objet complet
|
|
"delivery_address": address,
|
|
"coordinates": gin.H{
|
|
"latitude": location.Latitude,
|
|
"longitude": location.Longitude,
|
|
},
|
|
"queue_info": queueInfo,
|
|
})
|
|
}
|
|
|
|
// ============================================
|
|
// ASSIGNATION EN MASSE (TOUTES LES COMMANDES PENDING)
|
|
// ============================================
|
|
|
|
// AutoAssignAllPendingCommands assigne toutes les commandes en attente
|
|
// POST /api/v2/admin/protected/commands/auto-assign-all
|
|
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
|
|
}
|
|
|
|
// Récupérer toutes les commandes pending
|
|
commands, err := database.GetAllCommands("pending", "")
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"error": "Erreur récupération des commandes",
|
|
"details": err.Error(),
|
|
})
|
|
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 {
|
|
// Essayer avec float64
|
|
if idFloat, ok := cmd["id"].(float64); ok {
|
|
commandID = int(idFloat)
|
|
} else {
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Récupérer l'adresse
|
|
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
|
|
}
|
|
|
|
// Géocoder l'adresse
|
|
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,
|
|
}
|
|
|
|
// Récupérer les livreurs actifs
|
|
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
|
|
}
|
|
|
|
// Trouver le livreur le plus proche (version rapide pour assignation masse)
|
|
nearest, err := geoService.FindNearestDeliveryPersonFast(targetCoords, usernames)
|
|
if err != nil {
|
|
failed = append(failed, gin.H{
|
|
"command_id": commandID,
|
|
"error": "Aucun livreur avec position GPS",
|
|
})
|
|
continue
|
|
}
|
|
|
|
// Pour l'assignation en masse, on utilise le calcul rapide
|
|
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
|
|
}
|
|
|
|
// Mettre à jour le statut du livreur
|
|
database.SetDeliveryPersonStatus(nearest.Username, "busy", commandID)
|
|
|
|
// Récupérer l'ETA - ✅ CORRECTION: Gérer les types correctement
|
|
etaData, _ := database.GetCommandETA(commandID)
|
|
|
|
var totalETA, waitTime interface{}
|
|
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)
|
|
}
|
|
|
|
// Récupérer l'overview des queues
|
|
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",
|
|
})
|
|
}
|
|
|
|
// ============================================
|
|
// RÉCUPÉRER L'ÉTAT DES QUEUES DES LIVREURS
|
|
// ============================================
|
|
|
|
// GetAllDeliveryQueues retourne l'état de toutes les queues des livreurs
|
|
// GET /api/v2/admin/protected/delivery/queues
|
|
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",
|
|
"details": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
// Récupérer les détails de chaque livreur
|
|
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]interface{}
|
|
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,
|
|
})
|
|
}
|
|
|
|
// ============================================
|
|
// RÉCUPÉRER LA QUEUE D'UN LIVREUR SPÉCIFIQUE
|
|
// ============================================
|
|
|
|
// GetDeliverymanQueue retourne la queue d'un livreur spécifique
|
|
// GET /api/v2/admin/protected/delivery/:username/queue
|
|
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",
|
|
"details": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"queue_info": queueInfo,
|
|
})
|
|
}
|
|
|
|
// ============================================
|
|
// VALIDATION D'ADRESSE
|
|
// ============================================
|
|
|
|
// ValidateAddress vérifie si une adresse peut être géocodée
|
|
// POST /api/v1/validate-address
|
|
// Body: {"address": "123 Main St, Paris"}
|
|
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",
|
|
"details": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
isValid := geoService.IsValidAddress(req.Address)
|
|
|
|
if !isValid {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"valid": false,
|
|
"message": "Adresse introuvable ou invalide",
|
|
})
|
|
return
|
|
}
|
|
|
|
// Récupérer les détails
|
|
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,
|
|
})
|
|
}
|
|
|
|
// ============================================
|
|
// HELPER FUNCTION - CALCUL ETA AVEC TOMTOM
|
|
// ============================================
|
|
|
|
// calculateTravelTimeWithTomTom calcule l'ETA avec TomTom ou fallback local
|
|
func calculateTravelTimeWithTomTom(geoService *services.GeoService, deliverymanUsername string, targetLat, targetLon float64) (int, float64, error) {
|
|
// Récupérer position du livreur
|
|
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,
|
|
}
|
|
|
|
// Calculer ETA avec TomTom (avec fallback automatique intégré)
|
|
travelTime, distance, err := services.GetETAWithTraffic(*deliverymanLoc, targetCoords)
|
|
if err != nil {
|
|
// Fallback sur calcul local
|
|
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
|
|
}
|