package db import ( "encoding/json" "fmt" "gestion/models" "gestion/services" "log" "math" "sort" "strconv" "time" "github.com/redis/go-redis/v9" ) // CompleteDeliveryAndProcessNext marque une livraison comme terminée et optimise la queue func (d *Database) CompleteDeliveryAndProcessNext(deliveryman string, completedCommandID int) error { queueKey := fmt.Sprintf("queue:deliveryman:%s", deliveryman) // Retirer la commande complétée de la queue Redis.ZRem(RedisCtx, queueKey, strconv.Itoa(completedCommandID)) // Supprimer les données de la commande commandKey := fmt.Sprintf("queue:pending:%d", completedCommandID) Redis.Del(RedisCtx, commandKey) // Supprimer le cache de destination destCacheKey := fmt.Sprintf("command:destination:%d", completedCommandID) Redis.Del(RedisCtx, destCacheKey) // Supprimer l'ETA etaKey := fmt.Sprintf("command:eta:%d", completedCommandID) Redis.Del(RedisCtx, etaKey) // Décrémenter le compteur counterKey := fmt.Sprintf("queue:deliveryman:%s:count", deliveryman) Redis.Decr(RedisCtx, counterKey) log.Printf("✅ Livraison %d complétée par %s", completedCommandID, deliveryman) // Vérifier s'il reste des commandes remainingCount, _ := Redis.ZCard(RedisCtx, queueKey).Result() if remainingCount == 0 { d.SetDeliveryPersonStatus(deliveryman, "available", 0) log.Printf("🔓 Livreur %s libéré - Plus de commandes en queue", deliveryman) // Chercher dans la queue générale go d.ProcessNextCommandInQueue(deliveryman) return nil } // ============================================ // 🔄 OPTIMISATION PAR PROXIMITÉ // ============================================ log.Printf("🔄 Optimisation queue de %s: %d commande(s) restante(s)", deliveryman, remainingCount) err := d.OptimizeDeliverymanQueueByProximity(deliveryman) if err != nil { log.Printf("⚠️ Erreur optimisation queue: %v", err) // Fallback: recalculer les ETAs sans réorganiser d.RecalculateQueueETAs(deliveryman) } // Récupérer la prochaine commande (maintenant la plus proche) nextCommand, nextETA, err := d.FindNearestCommandInQueue(deliveryman) if err == nil && nextCommand != nil { d.SetDeliveryPersonStatus(deliveryman, "busy", nextCommand.CommandID) log.Printf("📍 Prochaine livraison: Commande %d (ETA: %d min)", nextCommand.CommandID, nextETA) } return nil } func (d *Database) OptimizeDeliverymanQueueByProximity(deliveryman string) error { livreurLat, livreurLng, err := d.GetDeliveryPersonLocation(deliveryman) if err != nil { log.Printf("⚠️ Position livreur %s non disponible, recalcul ETAs simple", deliveryman) return d.RecalculateQueueETAs(deliveryman) } queueKey := fmt.Sprintf("queue:deliveryman:%s", deliveryman) commandIDs, err := Redis.ZRange(RedisCtx, queueKey, 0, -1).Result() if err != nil || len(commandIDs) == 0 { return nil } // Collecter les informations de chaque commande avec sa distance var commandsWithDistance []CommandWithDistance for _, cmdIDStr := range commandIDs { commandID := extractCommandID(cmdIDStr) if commandID <= 0 { continue } // 1. Essayer de récupérer depuis queue:pending:{id} commandKey := fmt.Sprintf("queue:pending:%d", commandID) data, err := Redis.Get(RedisCtx, commandKey).Result() var lat, lng float64 var address string if err == nil && data != "" { var queueItem models.CommandQueue if err := json.Unmarshal([]byte(data), &queueItem); err == nil { lat = queueItem.Lat lng = queueItem.Lng address = queueItem.Address } } // 2. Si coordonnées à 0, essayer le cache command:destination:{id} if lat == 0 && lng == 0 { destCacheKey := fmt.Sprintf("command:destination:%d", commandID) destData, err := Redis.Get(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 { lat = coords.Lat lng = coords.Lon log.Printf("📍 Coordonnées récupérées depuis cache destination pour commande %d: (%.6f, %.6f)", commandID, lat, lng) } } } // 3. Si toujours 0, récupérer depuis la DB if lat == 0 && lng == 0 { command, err := d.GetCommandByID(commandID) if err == nil { if dLat, ok := command["dest_latitude"].(float64); ok && dLat != 0 { lat = dLat } if dLng, ok := command["dest_longitude"].(float64); ok && dLng != 0 { lng = dLng } if address == "" { if addr, ok := command["adresse"].(string); ok { address = addr } } } } // 4. Si toujours 0, utiliser une distance très grande if lat == 0 && lng == 0 { log.Printf("⚠️ Coordonnées non disponibles pour commande %d, utilisation position par défaut", commandID) commandsWithDistance = append(commandsWithDistance, CommandWithDistance{ CommandID: commandID, Address: address, Lat: 0, Lng: 0, Distance: 999999, EstimatedETA: 120, QueueItem: models.CommandQueue{ CommandID: commandID, Address: address, Lat: 0, Lng: 0, }, }) continue } // ✅ MODIFIÉ: Distance depuis la position ACTUELLE du livreur (pas chaînée) distance := d.CalculateDistanceBetweenPoints(livreurLat, livreurLng, lat, lng) commandsWithDistance = append(commandsWithDistance, CommandWithDistance{ CommandID: commandID, Address: address, Lat: lat, Lng: lng, Distance: distance, EstimatedETA: services.CalculateETA(distance), QueueItem: models.CommandQueue{ CommandID: commandID, Address: address, Lat: lat, Lng: lng, }, }) } if len(commandsWithDistance) == 0 { return nil } // Trier par distance (la plus proche en premier) sort.Slice(commandsWithDistance, func(i, j int) bool { return commandsWithDistance[i].Distance < commandsWithDistance[j].Distance }) log.Printf("🔄 Optimisation queue de %s: %d commandes triées par proximité", deliveryman, len(commandsWithDistance)) // Vider la queue actuelle Redis.Del(RedisCtx, queueKey) Redis.Del(RedisCtx, fmt.Sprintf("queue:deliveryman:%s:count", deliveryman)) // ✅ MODIFIÉ: Recréer la queue avec ETA = distance directe depuis position livreur for i, cmd := range commandsWithDistance { var travelTime int var distance float64 if cmd.Lat != 0 && cmd.Lng != 0 { // ✅ Distance depuis la position ACTUELLE du livreur (pas cumulative) distance = d.CalculateDistanceBetweenPoints(livreurLat, livreurLng, cmd.Lat, cmd.Lng) travelTime = services.CalculateETA(distance) } else { distance = 0 travelTime = 10 } // ✅ ETA = temps de trajet direct uniquement cmd.QueueItem.EstimatedETA = travelTime score := float64(i + 1) data, _ := json.Marshal(cmd.QueueItem) commandKey := fmt.Sprintf("queue:pending:%d", cmd.CommandID) Redis.Set(RedisCtx, commandKey, data, 24*time.Hour) Redis.ZAdd(RedisCtx, queueKey, redis.Z{ Score: score, Member: cmd.CommandID, }) d.SetCommandETAWithDetails(cmd.CommandID, travelTime, i+1) log.Printf(" 📍 Position %d: Commande %d - %.2f km - ETA trajet: %d min", i+1, cmd.CommandID, distance, travelTime) } Redis.Set(RedisCtx, fmt.Sprintf("queue:deliveryman:%s:count", deliveryman), len(commandsWithDistance), 0) log.Printf("✅ Queue de %s optimisée: %d commandes réorganisées par proximité", deliveryman, len(commandsWithDistance)) return nil } func (d *Database) RecalculateQueueETAs(deliveryman string) error { queueKey := fmt.Sprintf("queue:deliveryman:%s", deliveryman) commandIDs, err := Redis.ZRange(RedisCtx, queueKey, 0, -1).Result() if err != nil || len(commandIDs) == 0 { return nil } // ✅ Récupérer la position actuelle du livreur livreurLat, livreurLng, err := d.GetDeliveryPersonLocation(deliveryman) if err != nil { log.Printf("⚠️ Position livreur %s non disponible pour recalcul ETA", deliveryman) return err } for i, cmdIDStr := range commandIDs { commandID := extractCommandID(cmdIDStr) if commandID <= 0 { continue } commandKey := fmt.Sprintf("queue:pending:%d", commandID) data, err := Redis.Get(RedisCtx, commandKey).Result() if err != nil { continue } var queueItem models.CommandQueue if err := json.Unmarshal([]byte(data), &queueItem); err != nil { continue } // ✅ MODIFIÉ: ETA = distance directe depuis position livreur travelTime := d.CalculateETABetweenPoints(livreurLat, livreurLng, queueItem.Lat, queueItem.Lng) d.SetCommandETAWithDetails(commandID, travelTime, i+1) queueItem.EstimatedETA = travelTime updatedData, _ := json.Marshal(queueItem) Redis.Set(RedisCtx, commandKey, updatedData, 24*time.Hour) } log.Printf("🔄 ETAs recalculés pour %d commandes de %s (trajet direct)", len(commandIDs), deliveryman) return nil } func (d *Database) UpdateQueueETAsAfterCompletion(deliveryman string) error { return d.RecalculateQueueETAs(deliveryman) } // FindNearestCommandInQueue trouve la commande la plus proche du livreur func (d *Database) FindNearestCommandInQueue(deliveryman string) (*models.CommandQueue, int, error) { livreurLat, livreurLng, err := d.GetDeliveryPersonLocation(deliveryman) if err != nil { return nil, 0, fmt.Errorf("position livreur non disponible: %w", err) } queueKey := fmt.Sprintf("queue:deliveryman:%s", deliveryman) commandIDs, err := Redis.ZRange(RedisCtx, queueKey, 0, -1).Result() if err != nil || len(commandIDs) == 0 { return nil, 0, fmt.Errorf("queue vide") } var nearestCommand *models.CommandQueue var nearestDistance float64 = math.MaxFloat64 var nearestETA int for _, cmdIDStr := range commandIDs { commandID := extractCommandID(cmdIDStr) if commandID <= 0 { continue } commandKey := fmt.Sprintf("queue:pending:%d", commandID) data, err := Redis.Get(RedisCtx, commandKey).Result() if err != nil { continue } var queueItem models.CommandQueue if err := json.Unmarshal([]byte(data), &queueItem); err != nil { continue } distance := d.CalculateDistanceBetweenPoints(livreurLat, livreurLng, queueItem.Lat, queueItem.Lng) if distance < nearestDistance { nearestDistance = distance nearestCommand = &queueItem nearestETA = services.CalculateETA(distance) } } if nearestCommand == nil { return nil, 0, fmt.Errorf("aucune commande trouvée") } return nearestCommand, nearestETA, nil }