Files
projet_gestion_commande/backend/gestion/handlers/geoloca.go
T
Xor290 22a8d5026c
Frontend Admin - EAS Build / build (push) Canceled after 0s
Frontend Client - EAS Build / build (push) Canceled after 0s
Backend - Build & Lint / build (push) Failing after 25m18s
Frontend Web - Build & Lint / build (push) Failing after 9m58s
chore: build
2026-08-06 12:06:05 +02:00

795 lines
23 KiB
Go

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
}