package handlers import ( "encoding/json" "fmt" "gestion/db" "gestion/models" "gestion/services" "gestion/utils" "log" "net/http" "slices" "strconv" "github.com/gin-gonic/gin" ) 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", }) return } // Collecter tous les IDs et usernames en une passe pour éviter les N+1 commandIDs := make([]int, 0, len(commands)) clientUsernames := make([]string, 0, len(commands)) for _, cmd := range commands { if cid, _ := strconv.Atoi(fmt.Sprintf("%v", cmd["id"])); cid > 0 { commandIDs = append(commandIDs, cid) } if u, _ := cmd["username"].(string); u != "" { clientUsernames = append(clientUsernames, u) } } allItems, _ := database.GetCommandItemsBatch(commandIDs) allClients, _ := database.GetClientsByUsernames(clientUsernames) filteredCommands := make([]gin.H, len(commands)) for i, cmd := range commands { commandID, _ := strconv.Atoi(fmt.Sprintf("%v", cmd["id"])) items := allItems[commandID] clientUsername, _ := cmd["username"].(string) client := allClients[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"], "promo_discount": item["promo_discount"], "is_reward": item["is_reward"], } } etaData, _ := database.GetCommandETA(commandID) filteredCommands[i] = gin.H{ "id": cmd["id"], "status": cmd["status"], "adresse": cmd["adresse"], "total_prix": cmd["total_prix"], "referral_used": cmd["referral_used"], "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"], "promo_discount": item["promo_discount"], "is_reward": item["is_reward"], } } 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"], "referral_used": command["referral_used"], "created_at": command["created_at"], "client_info": clientInfo, "items": itemsSummary, "items_count": len(items), "eta": etaData, }, }) } 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", }) 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 } 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 } validStatuses := []string{ "assigned", "en_route", "arrived", "livre", "cancelled", } if !slices.Contains(validStatuses, req.Status) { c.JSON(http.StatusBadRequest, gin.H{ "error": "Statut invalide", "valid_statuses": validStatuses, "received": req.Status, }) return } if req.Status == "livre" { if req.Latitude == 0 || req.Longitude == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "Coordonnées GPS requises pour confirmer la livraison"}) return } destLat, _ := command["dest_latitude"].(float64) destLon, _ := command["dest_longitude"].(float64) if destLat != 0 && destLon != 0 { distance := utils.CalculateDistance(req.Latitude, req.Longitude, destLat, destLon) log.Printf("📍 [GPS] Distance: %.2f m", distance) log.Printf("✅ [GPS] Validation OK") } else { log.Printf("⚠️ [GPS] Coordonnées de destination non disponibles, validation ignorée") } } // Mettre à jour le statut. // Le cas "cancelled" passe par une transaction atomique dédiée (transition + // remboursement stock), pour empêcher tout double remboursement en cas de // double appel (double-tap, retry réseau, commande déjà annulée ailleurs). if req.Status == "cancelled" { alreadyCancelled, prevStatus, cancelErr := database.CancelDeliveryByLivreurAtomic(commandID) if cancelErr != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": "Erreur mise à jour", }) return } if alreadyCancelled { c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Commande déjà annulée", "command_id": commandID, "status": "cancelled", }) return } cancelMsg := req.Notes if cancelMsg == "" { cancelMsg = "Annulé par le livreur" } database.SetCommandCancelReason(commandID, fmt.Sprintf("[Livreur: %s] %s", usernameStr, cancelMsg)) if prevStatus == "arrived" || prevStatus == "livre" { clientUsername, _ := command["username"].(string) if clientUsername != "" { if penalty, err := database.ApplyCancellationPenalty(clientUsername); err == nil { log.Printf("⚠️ [CANCEL_LIVREUR] Amende %d appliquée à %s (client absent)", penalty, clientUsername) } else { log.Printf("⚠️ [CANCEL_LIVREUR] Erreur application amende pour %s: %v", clientUsername, err) } } } } else if err := database.UpdateCommandStatus(commandID, req.Status); err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": "Erreur mise à jour", }) 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...") var destLat, destLon float64 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) } } if destLat != 0 && destLon != 0 { toCoords := services.Coordinates{Latitude: destLat, Longitude: destLon} // Cas 1 : GPS du livreur disponible gpsLat, gpsLon, gpsErr := database.GetDeliveryPersonLocation(usernameStr) if gpsErr == nil && gpsLat != 0 { from := services.Coordinates{Latitude: gpsLat, Longitude: gpsLon} eta, _, err := services.GetETAWithTraffic(from, toCoords) if err != nil { eta = services.CalculateETA(services.CalculateDistance(from, toCoords)) } etaMinutes = eta log.Printf("📍 [STATUS_LIVREUR] ETA depuis GPS livreur: %d min", etaMinutes) } else { // Cas 2 : GPS absent → dernière adresse de livraison lastLat, lastLon, lastErr := database.GetLastDeliveryCoords(usernameStr) if lastErr == nil && lastLat != 0 { from := services.Coordinates{Latitude: lastLat, Longitude: lastLon} eta, _, err := services.GetETAWithTraffic(from, toCoords) if err != nil { eta = services.CalculateETA(services.CalculateDistance(from, toCoords)) } etaMinutes = eta log.Printf("📍 [STATUS_LIVREUR] ETA depuis dernière livraison: %d min", etaMinutes) } else { // Cas 3 : Aucune position disponible etaMinutes = 30 log.Printf("⚠️ [STATUS_LIVREUR] Aucune position disponible - ETA par défaut: %d min", etaMinutes) } } } else { etaMinutes = 30 log.Printf("⚠️ [STATUS_LIVREUR] Coordonnées destination manquantes - ETA par défaut: %d min", etaMinutes) } database.SetCommandETA(commandID, etaMinutes) log.Printf("✅ [STATUS_LIVREUR] ETA défini: %d minutes", etaMinutes) if etaMinutes >= 60 { h := etaMinutes / 60 m := etaMinutes % 60 if m > 0 { etaMessage = fmt.Sprintf("Arrivée prévue dans %dh%02d", h, m) } else { etaMessage = fmt.Sprintf("Arrivée prévue dans %dh", h) } } else { etaMessage = fmt.Sprintf("Arrivée prévue dans %d minutes", etaMinutes) } // 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 = utils.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 "en_route": notifETA := etaMinutes if notifETA == 0 { if etaData, err := database.GetCommandETA(commandID); err == nil { if v, ok := etaData["total_eta_minutes"]; ok { if n, err2 := strconv.Atoi(v); err2 == nil && n > 0 { notifETA = n } } } } if notifETA > 0 { var etaStr string if notifETA >= 60 { h := notifETA / 60 m := notifETA % 60 if m > 0 { etaStr = fmt.Sprintf("%dh%02d", h, m) } else { etaStr = fmt.Sprintf("%dh", h) } } else { etaStr = fmt.Sprintf("%d min", notifETA) } clientMsg = fmt.Sprintf("Un livreur vient de prendre en charge ta commande #%d, il est en route (~%s)", database.GetClientOrderID(commandID), etaStr) } else { clientMsg = fmt.Sprintf("Un livreur vient de prendre en charge ta commande #%d, il est en route.", database.GetClientOrderID(commandID)) } case "arrived": clientMsg = fmt.Sprintf("Descend, le livreur est là dans 3min (commande #%d) 🛵", database.GetClientOrderID(commandID)) case "livre": clientMsg = fmt.Sprintf("Ta commande #%d a bien été livrée ! Bonne dégustation l'ami et à bientôt 😊\n\n⚠️ VALIDE LA RÉCEPTION DE TA COMMANDE DANS LA RUBRIQUE SUIVI POUR RÉCUPÉRER TES POINTS DE FIDÉLITÉ ⚠️", database.GetClientOrderID(commandID)) case "cancelled": clientMsg = fmt.Sprintf("Votre commande #%d a été annulée par le livreur", database.GetClientOrderID(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 "cancelled": // Transition + remboursement stock déjà effectués atomiquement plus haut. log.Printf("🚫 Livraison annulée par livreur - Nettoyage queue cmd %d", commandID) database.CompleteDeliveryAndProcessNext(usernameStr, commandID) 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) } // POST /api/v1/livreur/deliveries/:id/issue func ReportDeliveryIssue(c *gin.Context) { username := c.GetString("username") if c.GetString("role") != "livreur" { 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 invalide"}) return } var req struct { IssueType string `json:"issue_type" binding:"required"` Description string `json:"description"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "issue_type requis"}) return } validTypes := map[string]bool{ "client_absent": true, "wrong_address": true, "refused_delivery": true, "no_access": true, "other": true, } if !validTypes[req.IssueType] { c.JSON(http.StatusBadRequest, gin.H{"error": "Type de problème invalide"}) return } database := c.MustGet("database").(*db.Database) command, err := database.GetCommandByID(commandID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Commande introuvable"}) return } if livreur, _ := command["livreur_assign"].(string); livreur != username { c.JSON(http.StatusForbidden, gin.H{"error": "Commande non assignée à vous"}) return } issue, err := database.CreateDeliveryIssue(commandID, req.IssueType, req.Description, username) if err != nil { log.Printf("❌ [ISSUE] Erreur création: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur création problème"}) return } log.Printf("📋 [ISSUE] Créé par %s pour commande #%d: %s", username, commandID, req.IssueType) c.JSON(http.StatusCreated, gin.H{"success": true, "issue": issue}) } func GetMyDeliveryStats(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) var dayRows []models.DayRowWithResult if err := database.GetMyDeliveryStatsPerDay(&dayRows, usernameStr); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur récupération stats jour"}) return } var weekRows []models.WeekRow if err := database.GetMyDeliveryStatsPerWeek(&weekRows, usernameStr); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur récupération stats semaine"}) return } var monthRows []models.MonthRow if err := database.GetMyDeliveryStatsPerMonth(&monthRows, usernameStr); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur récupération stats mois"}) return } var todayRow models.TodayRow if err := database.GetMyDeliveryStatsToday(&todayRow, usernameStr); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur récupération stats du jour"}) return } monthNames := [13]string{"", "Jan", "Fév", "Mar", "Avr", "Mai", "Jun", "Jul", "Aoû", "Sep", "Oct", "Nov", "Déc"} byDay := make([]gin.H, len(dayRows)) for i, r := range dayRows { byDay[i] = gin.H{ "label": r.Day.Format("02/01"), "count": r.Count, "revenue": r.Revenue, } } byWeek := make([]gin.H, len(weekRows)) for i, r := range weekRows { byWeek[i] = gin.H{ "label": fmt.Sprintf("S%d", r.WeekNum), "count": r.Count, "revenue": r.Revenue, } } byMonth := make([]gin.H, len(monthRows)) for i, r := range monthRows { label := "?" if r.MonthNum >= 1 && r.MonthNum <= 12 { label = monthNames[r.MonthNum] } byMonth[i] = gin.H{ "label": label, "count": r.Count, "revenue": r.Revenue, } } c.JSON(http.StatusOK, gin.H{ "success": true, "by_day": byDay, "by_week": byWeek, "by_month": byMonth, "today_count": todayRow.Count, "today_revenue": todayRow.Revenue, }) }