// ============================================ // handlers/commands_handlers_CORRIGES.go // ============================================ // ⚠️ CreateCommandFromBasket SUPPRIMÉ (utiliser ValidateBasket à la place) package handlers import ( "fmt" "gestion/db" "log" "net/http" "strconv" "strings" "time" "github.com/gin-gonic/gin" ) var ( rateLimitMap = make(map[string][]time.Time) maxRequests = 10 timeWindow = time.Minute ) func checkRateLimit(key string) bool { now := time.Now() if timestamps, exists := rateLimitMap[key]; exists { // Nettoyer les anciennes entrées var validTimestamps []time.Time for _, ts := range timestamps { if now.Sub(ts) < timeWindow { validTimestamps = append(validTimestamps, ts) } } rateLimitMap[key] = validTimestamps if len(validTimestamps) >= maxRequests { return false } } rateLimitMap[key] = append(rateLimitMap[key], now) return true } // ============================================ // GESTION ADRESSE & ADMIN // ============================================ func safeGetUsername(c *gin.Context) (string, error) { username, exists := c.Get("username") if !exists { return "", fmt.Errorf("utilisateur non authentifié") } usernameStr, ok := username.(string) if !ok || usernameStr == "" { return "", fmt.Errorf("username invalide") } return usernameStr, nil } func validateAddress(address string) error { if len(address) == 0 { return fmt.Errorf("adresse vide") } if len(address) > 500 { return fmt.Errorf("adresse trop longue (max 500 caractères)") } if strings.TrimSpace(address) == "" { return fmt.Errorf("adresse invalide") } return nil } // UpdateCommandAddress met à jour l'adresse de livraison d'une commande // PUT /api/v1/admin/commands/:id/address func UpdateCommandAddress(c *gin.Context) { database := c.MustGet("database").(*db.Database) // ✅ Vérification du rôle if c.GetString("role") != "admin" { log.Printf("❌ [UPD_ADDR] Accès refusé - role=%s", c.GetString("role")) c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"}) return } // ✅ Récupération sécurisée du username adminUsername, err := safeGetUsername(c) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) return } // ✅ Rate limiting rateLimitKey := fmt.Sprintf("update_addr:%s", adminUsername) if !checkRateLimit(rateLimitKey) { log.Printf("⚠️ [UPD_ADDR] Rate limit dépassé pour %s", adminUsername) c.JSON(http.StatusTooManyRequests, gin.H{"error": "Trop de requêtes, réessayez plus tard"}) return } commandID, err := strconv.Atoi(c.Param("id")) if err != nil || commandID <= 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "ID de commande invalide"}) return } var req struct { DeliveryAddress string `json:"delivery_address" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"}) return } // ✅ Validation de l'adresse if err := validateAddress(req.DeliveryAddress); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // ✅ Logs sanitizés log.Printf("📝 [UPD_ADDR] Admin %s modifie cmd %d", adminUsername, commandID) command, err := database.GetCommandByID(commandID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Commande non trouvée"}) return } status, _ := command["status"].(string) if status == "livre" || status == "approved" || status == "cancelled" { c.JSON(http.StatusBadRequest, gin.H{ "error": "Impossible de modifier l'adresse d'une commande terminée", }) return } if err := database.UpdateCommandAddress(commandID, req.DeliveryAddress); err != nil { log.Printf("❌ [UPD_ADDR] Erreur DB: %v", err) c.JSON(http.StatusInternalServerError, gin.H{ "error": "Erreur lors de la mise à jour", }) return } database.AddCommandLog(commandID, "address_updated", fmt.Sprintf("Adresse mise à jour par admin %s", adminUsername), adminUsername) log.Printf("✅ [UPD_ADDR] Commande %d mise à jour", commandID) c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Adresse de livraison mise à jour", "command_id": commandID, }) } func GetAllCommands(c *gin.Context) { database := c.MustGet("database").(*db.Database) status := c.Query("status") username := c.Query("username") userRole := c.GetString("role") if userRole != "admin" && userRole != "cabine" { log.Printf("❌ [VALIDATE] Accès refusé - role=%s", userRole) c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"}) return } if username == "" { usernameParam := c.Param("username") if usernameParam != "" && usernameParam != "all" { username = usernameParam } } if status == "" { statusParam := c.Param("status") if statusParam != "" && statusParam != "all" { status = statusParam } } log.Printf("🔍 [GET_CMDS] Filtre - status=[%s], username=[%s]", status, username) commands, err := database.GetAllCommands(status, username) if err != nil { log.Printf("❌ [GET_CMDS] Erreur: %v", err) c.JSON(http.StatusInternalServerError, gin.H{ "error": "Erreur lors de la récupération des commandes", "details": err.Error(), }) return } // ✅ MODIFICATION: Pour le count, utiliser len(commands) au lieu de GetCommandCount() // Cela permet d'avoir le vrai nombre de commandes retournées countCommand := len(commands) log.Printf("✅ [GET_CMDS] Trouvées: %d commandes", countCommand) c.JSON(http.StatusOK, gin.H{ "success": true, "commands": commands, "count": countCommand, }) } // GetCommandByID récupère une commande complète avec logs // GET /api/v1/commands/:id func GetCommandByID(c *gin.Context) { database := c.MustGet("database").(*db.Database) username, err := safeGetUsername(c) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentification requise"}) return } userRole := c.GetString("role") commandID, err := strconv.Atoi(c.Param("id")) if err != nil || commandID <= 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "ID de commande invalide"}) return } log.Printf("📋 [GET_CMD] User %s (%s) récupère cmd %d", username, userRole, commandID) command, err := database.GetCommandByID(commandID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Commande non trouvée"}) return } // ✅ CRITIQUE: Vérification de propriété cmdUsername, _ := command["username"].(string) // Seuls l'admin, la cabine ou le propriétaire peuvent voir la commande if userRole != "admin" && userRole != "cabine" && cmdUsername != username { log.Printf("❌ [GET_CMD] Accès refusé - User %s tente d'accéder à cmd de %s", username, cmdUsername) c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"}) return } // ✅ Logs uniquement pour admin/cabine if userRole == "admin" || userRole == "cabine" { logs, err := database.GetCommandLogs(commandID) if err == nil { command["logs"] = logs } } log.Printf("✅ [GET_CMD] Commande %d récupérée", commandID) c.JSON(http.StatusOK, gin.H{ "success": true, "command": command, }) } func ApproveDelivery(c *gin.Context) { database := c.MustGet("database").(*db.Database) username, err := safeGetUsername(c) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentification requise"}) return } // ✅ Rate limiting rateLimitKey := fmt.Sprintf("approve:%s", username) if !checkRateLimit(rateLimitKey) { c.JSON(http.StatusTooManyRequests, gin.H{"error": "Trop de requêtes"}) return } commandID, err := strconv.Atoi(c.Param("id")) if err != nil || commandID <= 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "ID invalide"}) return } log.Printf("✅ [APPROVE] Client %s approuve cmd %d", username, commandID) // ✅ TRANSACTION ATOMIQUE dans la DB pour éviter race condition // Cette fonction doit être créée dans le fichier db totalPoints, err := database.ApproveDeliveryAtomic(commandID, username) if err != nil { log.Printf("❌ [APPROVE] Erreur: %v", err) // ❌ Ne pas exposer les détails de l'erreur c.JSON(http.StatusBadRequest, gin.H{ "error": "Impossible d'approuver la livraison", }) return } log.Printf("✅ [APPROVE] %d points attribués à %s", totalPoints, username) c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Livraison confirmée", "command_id": commandID, "points_earned": totalPoints, }) } // ============================================ // APPROBATION PAR ADMIN // ============================================ func ValidateDelivery(c *gin.Context) { database := c.MustGet("database").(*db.Database) if c.GetString("role") != "admin" { c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"}) return } adminUsername, err := safeGetUsername(c) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) return } // ✅ Rate limiting rateLimitKey := fmt.Sprintf("validate:%s", adminUsername) if !checkRateLimit(rateLimitKey) { c.JSON(http.StatusTooManyRequests, gin.H{"error": "Trop de requêtes"}) return } var req struct { CommandID int `json:"command_id"` CommandIDs []int `json:"command_ids"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"}) return } commandIDs := req.CommandIDs if req.CommandID > 0 && len(commandIDs) == 0 { commandIDs = []int{req.CommandID} } // ✅ LIMITE sur le nombre d'IDs const maxCommandIDs = 50 if len(commandIDs) == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "Aucun ID fourni"}) return } if len(commandIDs) > maxCommandIDs { c.JSON(http.StatusBadRequest, gin.H{ "error": fmt.Sprintf("Maximum %d commandes à la fois", maxCommandIDs), }) return } // ✅ Validation des IDs for _, id := range commandIDs { if id <= 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "ID invalide"}) return } } log.Printf("📝 [VALIDATE] Admin %s valide %d commande(s)", adminUsername, len(commandIDs)) var validated []gin.H var failed []gin.H for _, commandID := range commandIDs { command, err := database.GetCommandByID(commandID) if err != nil { failed = append(failed, gin.H{ "command_id": commandID, "error": "Commande non trouvée", }) continue } currentStatus, _ := command["status"].(string) validStatuses := []string{"assigned", "en_route", "pending", "support", "livre"} isValid := false for _, s := range validStatuses { if currentStatus == s { isValid = true break } } if !isValid { failed = append(failed, gin.H{ "command_id": commandID, "error": "Statut invalide pour validation", }) continue } // ✅ Utiliser une transaction atomique pointsAwarded, err := database.ValidateDeliveryAtomic(commandID, adminUsername) if err != nil { log.Printf("❌ [VALIDATE] Erreur cmd %d: %v", commandID, err) failed = append(failed, gin.H{ "command_id": commandID, "error": "Erreur lors de la validation", }) continue } validated = append(validated, gin.H{ "command_id": commandID, "points_awarded": pointsAwarded, }) } log.Printf("✅ [VALIDATE] %d validées, %d échouées", len(validated), len(failed)) c.JSON(http.StatusOK, gin.H{ "success": true, "validated_count": len(validated), "failed_count": len(failed), "validated": validated, "failed": failed, }) } // ============================================ // GESTION ADMIN // ============================================ // GetAvailableDeliveryPersons récupère les livreurs disponibles // GET /api/v1/admin/delivery-persons/available func GetAvailableDeliveryPersons(c *gin.Context) { database := c.MustGet("database").(*db.Database) // ✅ SÉCURITÉ: Admin seulement userRole := c.GetString("role") if userRole != "admin" { c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"}) return } livreurs, err := database.GetAvailableDeliveryPersons() if err != nil { log.Printf("❌ [GET_LIVREURS] Erreur: %v", err) c.JSON(http.StatusInternalServerError, gin.H{ "error": "Erreur lors de la récupération des livreurs", "details": err.Error(), }) return } log.Printf("✅ [GET_LIVREURS] Trouvés: %d livreurs disponibles", len(livreurs)) c.JSON(http.StatusOK, gin.H{ "success": true, "livreurs": livreurs, "count": len(livreurs), }) } // AssignDeliveryPerson assigne manuellement un livreur à une commande // POST /api/v1/admin/commands/:id/assign func AssignDeliveryPerson(c *gin.Context) { database := c.MustGet("database").(*db.Database) // ✅ SÉCURITÉ: Admin seulement if c.GetString("role") != "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 } var req struct { LivreurUsername string `json:"livreur_username" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": "Données invalides", "details": err.Error(), }) return } log.Printf("👤 [ASSIGN] Assignation cmd %d à livreur %s", commandID, req.LivreurUsername) adminUsername, _ := c.Get("username") if err := database.AssignDeliveryPerson(commandID, req.LivreurUsername); err != nil { log.Printf("❌ [ASSIGN] Erreur: %v", err) c.JSON(http.StatusInternalServerError, gin.H{ "error": "Erreur assignation livreur", "details": err.Error(), }) return } database.AddCommandLog(commandID, "support", fmt.Sprintf("Livreur '%s' assigné manuellement par %s", req.LivreurUsername, adminUsername), adminUsername.(string)) log.Printf("✅ [ASSIGN] Commande %d assignée à %s", commandID, req.LivreurUsername) c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Livreur assigné avec succès", "command_id": commandID, "livreur": req.LivreurUsername, "assigned_by": adminUsername, }) } // DisableCommands désactive une ou plusieurs commandes // POST /api/v1/admin/commands/disable func DisableCommands(c *gin.Context) { database := c.MustGet("database").(*db.Database) var req struct { CommandIDs []int `json:"command_ids" binding:"required"` Reason string `json:"reason"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": "Données invalides", "details": err.Error(), }) return } if len(req.CommandIDs) == 0 { c.JSON(http.StatusBadRequest, gin.H{ "error": "Aucun ID de commande fourni", }) return } adminUsername, exists := c.Get("username") if !exists { adminUsername = "admin" } log.Printf("❌ [DISABLE] Désactivation de %d commande(s) par %s", len(req.CommandIDs), adminUsername) disabledCount := 0 failedCount := 0 errors := []string{} for _, commandID := range req.CommandIDs { command, err := database.GetCommandByID(commandID) if err != nil { errors = append(errors, "Commande "+strconv.Itoa(commandID)+" non trouvée") failedCount++ continue } currentStatus := command["status"].(string) if currentStatus == "disabled" { errors = append(errors, "Commande "+strconv.Itoa(commandID)+" déjà désactivée") failedCount++ continue } err = database.UpdateCommandStatus(commandID, "disabled") if err != nil { errors = append(errors, "Erreur désactivation cmd "+strconv.Itoa(commandID)) failedCount++ continue } reason := req.Reason if reason == "" { reason = "Désactivée par admin" } database.AddCommandLog(commandID, "disabled", reason, adminUsername.(string)) disabledCount++ log.Printf("✅ [DISABLE] Commande %d désactivée", commandID) } c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Traitement des commandes terminé", "disabled_count": disabledCount, "failed_count": failedCount, "errors": errors, }) } // ============================================ // COMMANDES CLIENT (My Orders) // ============================================ // GetMyCommands récupère les commandes du client authentifié // GET /api/v1/my-commands?status=pending func GetMyCommands(c *gin.Context) { database := c.MustGet("database").(*db.Database) // ✅ SÉCURITÉ: Récupérer depuis JWT validé username, exists := c.Get("username") if !exists { log.Printf("❌ [MY_CMDS] Utilisateur non authentifié") c.JSON(http.StatusUnauthorized, gin.H{ "error": "Utilisateur non authentifié", }) return } usernameStr := username.(string) status := c.Query("status") // ✅ NOUVEAU: Paramètre pour exclure les commandes approved excludeApproved := c.Query("exclude_approved") == "true" log.Printf("📋 [MY_CMDS] Récupération pour %s (status=%s, exclude_approved=%v)", usernameStr, status, excludeApproved) // ✅ Utiliser la nouvelle fonction avec filtrage commands, err := database.GetCommandsWithFilter(status, usernameStr, excludeApproved) if err != nil { log.Printf("❌ [MY_CMDS] Erreur: %v", err) c.JSON(http.StatusInternalServerError, gin.H{ "error": "Erreur lors de la récupération des commandes", "details": err.Error(), }) return } log.Printf("✅ [MY_CMDS] Trouvées: %d commandes", len(commands)) c.JSON(http.StatusOK, gin.H{ "success": true, "commands": commands, "count": len(commands), }) } func GetClientCommandsHistory(c *gin.Context) { database := c.MustGet("database").(*db.Database) username, exists := c.Get("username") if !exists { log.Printf("❌ [HISTORY] Utilisateur non authentifié") c.JSON(http.StatusUnauthorized, gin.H{"error": "Utilisateur non authentifié"}) return } usernameStr := username.(string) log.Printf("📚 [HISTORY] Récupération historique (approved) pour %s", usernameStr) commands, err := database.GetAllCommands("approved", usernameStr) if err != nil { log.Printf("❌ [HISTORY] Erreur: %v", err) c.JSON(http.StatusInternalServerError, gin.H{ "error": "Erreur récupération historique", "details": err.Error(), }) return } // ✅ AJOUTE CE LOG AVANT GetClientByUsername log.Printf("🔍 [HISTORY] AVANT GetClientByUsername pour: %s", usernameStr) client, err := database.GetClientByUsername(usernameStr) // ✅ AJOUTE CES LOGS APRÈS GetClientByUsername if err != nil { log.Printf("❌ [HISTORY] Erreur GetClientByUsername: %v", err) } else if client == nil { log.Printf("⚠️ [HISTORY] client est NIL!") } else { log.Printf("✅ [HISTORY] Client récupéré: username=%s, point=%d, point_zipette=%d", client.Username, client.Point, client.PointZipette) } resp := gin.H{ "success": true, "commands": commands, "count": len(commands), } if err == nil && client != nil { // ✅ VÉRIFIE AUSSI que client != nil resp["client_stats"] = gin.H{ "username": client.Username, "nom": client.Nom, "prenom": client.Prenom, "telephone": client.Telephone, "total_commands": client.Command, "points": client.Point, "points_zipette": client.PointZipette, "penalties": client.Amende, } log.Printf("✅ [HISTORY] Stats client: point=%d, point_zipette=%d", client.Point, client.PointZipette) } else { log.Printf("⚠️ [HISTORY] client_stats NON ajouté - err=%v, client=%v", err, client) } log.Printf("✅ [HISTORY] Historique: %d commandes approved", len(commands)) c.JSON(http.StatusOK, resp) } // ============================================ // DÉTAILS COMMANDES & ITEMS // ============================================ // ShowItems affiche les items d'une commande (Admin/Cabine) // GET /api/v1/admin/commands/:id/items func ShowItems(c *gin.Context) { database := c.MustGet("database").(*db.Database) userRole := c.GetString("role") // ✅ SÉCURITÉ: Admin ou Cabine seulement if userRole != "admin" && userRole != "cabine" { log.Printf("❌ [ITEMS] Accès refusé - role=%s", userRole) c.JSON(http.StatusForbidden, gin.H{ "error": "Accès refusé", "required_role": "admin ou cabine", "your_role": userRole, }) return } commandIDStr := c.Param("id") if commandIDStr == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "ID de commande manquant"}) return } commandID, err := strconv.Atoi(commandIDStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "ID de commande invalide"}) return } log.Printf("📦 [ITEMS] Récupération: cmd %d", commandID) items, err := database.GetCommandItems(commandID) if err != nil { log.Printf("❌ [ITEMS] Erreur: %v", err) c.JSON(http.StatusInternalServerError, gin.H{ "error": "Erreur lors de la récupération des items", "details": err.Error(), }) return } if len(items) == 0 { log.Printf("⚠️ [ITEMS] Aucun item trouvé") c.JSON(http.StatusOK, gin.H{ "success": true, "items": []map[string]interface{}{}, "count": 0, }) return } log.Printf("✅ [ITEMS] %d items récupérés", len(items)) commandInfo := map[string]interface{}{ "id": items[0]["command_id"], "status": items[0]["command_status"], "address": items[0]["command_address"], "total_prix": items[0]["total_prix"], "livreur": items[0]["livreur_assign"], "created_at": items[0]["command_created_at"], } clientInfo := map[string]interface{}{ "username": items[0]["client_username"], "nom": items[0]["client_nom"], "prenom": items[0]["client_prenom"], "telephone": items[0]["client_telephone"], } c.JSON(http.StatusOK, gin.H{ "success": true, "command_info": commandInfo, "client_info": clientInfo, "items": items, "count": len(items), }) } // GetCommandItemsWithDetails récupère les items enrichis // GET /api/v1/commands/:id/items/detailed func GetCommandItemsWithDetails(c *gin.Context) { database := c.MustGet("database").(*db.Database) // 🔐 Auth obligatoire username, exists := c.Get("username") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"}) return } userRole := c.GetString("role") usernameStr := username.(string) commandID, err := strconv.Atoi(c.Param("id")) if err != nil { log.Printf("❌ [ITEMS_DETAILED] ID invalide: %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": "ID de commande invalide"}) return } log.Printf("📦 [ITEMS_DETAILED] User=%s Role=%s Cmd=%d", usernameStr, userRole, commandID) // 🔒 ÉTAPE 1 — Vérifier l'accès à la commande allowed, err := database.CanUserAccessCommand(commandID, usernameStr, userRole) if err != nil { log.Printf("❌ [ITEMS_DETAILED] Erreur vérif accès: %v", err) c.JSON(http.StatusInternalServerError, gin.H{ "error": "Erreur vérification accès commande", }) return } if !allowed { log.Printf("🚨 [IDOR BLOCKED] User=%s Cmd=%d", usernameStr, commandID) c.JSON(http.StatusForbidden, gin.H{ "error": "Accès interdit à cette commande", }) return } // 🔓 ÉTAPE 2 — Accès autorisé, récupérer les items items, err := database.GetCommandItems(commandID) if err != nil { log.Printf("❌ [ITEMS_DETAILED] Erreur DB: %v", err) c.JSON(http.StatusInternalServerError, gin.H{ "error": "Erreur récupération items", "details": err.Error(), }) return } if len(items) == 0 { c.JSON(http.StatusNotFound, gin.H{ "error": "Aucun item trouvé pour cette commande", }) return } commandInfo := map[string]interface{}{ "id": items[0]["command_id"], "command_status": items[0]["command_status"], "command_address": items[0]["command_address"], "total_prix": items[0]["total_prix"], "livreur_assign": items[0]["livreur_assign"], "command_created_at": items[0]["command_created_at"], } c.JSON(http.StatusOK, gin.H{ "success": true, "command_info": commandInfo, "items": items, "count": len(items), "total_price": commandInfo["total_prix"], "client_info": gin.H{ "username": items[0]["client_username"], "nom": items[0]["client_nom"], "prenom": items[0]["client_prenom"], "telephone": items[0]["client_telephone"], }, }) } // GetClientCommandsItems récupère tous les items du client // GET /api/v1/clients/:username/commands/items func GetClientCommandsItems(c *gin.Context) { database := c.MustGet("database").(*db.Database) username := c.Param("username") if username == "" { log.Printf("❌ [CLIENT_ITEMS] Username manquant") c.JSON(http.StatusBadRequest, gin.H{"error": "Username requis"}) return } log.Printf("📦 [CLIENT_ITEMS] Récupération pour: %s", username) client, err := database.GetClientByUsername(username) if err != nil { log.Printf("❌ [CLIENT_ITEMS] Client non trouvé") c.JSON(http.StatusNotFound, gin.H{"error": "Client non trouvé"}) return } items, err := database.GetCommandItemsByUsername(username) if err != nil { log.Printf("❌ [CLIENT_ITEMS] Erreur: %v", err) c.JSON(http.StatusInternalServerError, gin.H{ "error": "Erreur lors de la récupération des items", "details": err.Error(), }) return } commandsMap := make(map[int][]map[string]interface{}) var commandIDs []int for _, item := range items { cmdID := int(item["command_id"].(float64)) if _, exists := commandsMap[cmdID]; !exists { commandIDs = append(commandIDs, cmdID) } commandsMap[cmdID] = append(commandsMap[cmdID], item) } log.Printf("✅ [CLIENT_ITEMS] %d items dans %d commandes", len(items), len(commandsMap)) type CommandGroup struct { CommandID int `json:"command_id"` Status string `json:"status"` Address string `json:"address"` TotalPrice float64 `json:"total_price"` LivreurAssign string `json:"livreur_assign"` CreatedAt string `json:"created_at"` ItemsCount int `json:"items_count"` Items []map[string]interface{} `json:"items"` } var commandGroups []CommandGroup for _, cmdID := range commandIDs { items := commandsMap[cmdID] if len(items) > 0 { group := CommandGroup{ CommandID: cmdID, Status: items[0]["command_status"].(string), Address: items[0]["command_address"].(string), TotalPrice: items[0]["total_prix"].(float64), LivreurAssign: fmt.Sprintf("%v", items[0]["livreur_assign"]), CreatedAt: items[0]["command_created_at"].(string), ItemsCount: len(items), Items: items, } commandGroups = append(commandGroups, group) } } c.JSON(http.StatusOK, gin.H{ "success": true, "client": client.Username, "client_name": fmt.Sprintf("%s %s", client.Prenom, client.Nom), "phone": client.Telephone, "total_items": len(items), "total_commands": len(commandGroups), "commands": commandGroups, }) } // UpdateItemStatus met à jour le statut d'un item // PUT /api/v1/admin/items/:item_id/status func UpdateItemStatus(c *gin.Context) { database := c.MustGet("database").(*db.Database) itemID, err := strconv.Atoi(c.Param("item_id")) if err != nil { log.Printf("❌ [UPD_ITEM] Erreur conversion ID: %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": "ID d'item invalide"}) return } var req struct { Status string `json:"status" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { log.Printf("❌ [UPD_ITEM] Erreur JSON: %v", err) c.JSON(http.StatusBadRequest, gin.H{ "error": "Status requis", "details": err.Error(), }) return } log.Printf("📝 [UPD_ITEM] Mise à jour: item=%d, status=%s", itemID, req.Status) validStatuses := []string{"pending", "preparing", "ready", "shipped", "delivered"} isValid := false for _, vs := range validStatuses { if req.Status == vs { isValid = true break } } if !isValid { log.Printf("❌ [UPD_ITEM] Statut invalide: %s", req.Status) c.JSON(http.StatusBadRequest, gin.H{ "error": "Statut invalide", "valid_statuses": validStatuses, "received_status": req.Status, }) return } err = database.UpdateCommandItemStatus(itemID, req.Status) if err != nil { log.Printf("❌ [UPD_ITEM] Erreur: %v", err) c.JSON(http.StatusInternalServerError, gin.H{ "error": "Erreur lors de la mise à jour", "details": err.Error(), }) return } log.Printf("✅ [UPD_ITEM] Item %d mise à jour: %s", itemID, req.Status) c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Statut de l'item mis à jour", "item_id": itemID, "new_status": req.Status, }) } // GetCommandFullDetails récupère tous les détails d'une commande // GET /api/v1/admin/commands/:id/full-details func GetCommandFullDetails(c *gin.Context) { database := c.MustGet("database").(*db.Database) userRole := c.GetString("role") if userRole != "admin" { log.Printf("❌ [FULL_DETAILS] Accès refusé - role=%s", userRole) c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé - Admin uniquement"}) return } commandID, err := strconv.Atoi(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "ID de commande invalide"}) return } log.Printf("📋 [FULL_DETAILS] Récupération complète: cmd %d", commandID) command, err := database.GetCommandByID(commandID) if err != nil { log.Printf("❌ [FULL_DETAILS] Non trouvée") c.JSON(http.StatusNotFound, gin.H{"error": "Commande non trouvée"}) return } items, _ := database.GetCommandItems(commandID) logs, _ := database.GetCommandLogs(commandID) var clientFullInfo map[string]interface{} if username, ok := command["username"].(string); ok { if client, err := database.GetClientByUsername(username); err == nil { clientFullInfo = map[string]interface{}{ "username": client.Username, "nom": client.Nom, "prenom": client.Prenom, "telephone": client.Telephone, "commands": client.Command, "points": client.Point, "penalties": client.Amende, "created_at": client.CreatedAt, } } } log.Printf("✅ [FULL_DETAILS] Récupéré: %d items, %d logs", len(items), len(logs)) c.JSON(http.StatusOK, gin.H{ "success": true, "command": command, "client_info": clientFullInfo, "items": items, "items_count": len(items), "logs": logs, "logs_count": len(logs), }) } // GetCommandItemsStats récupère les stats d'une commande // GET /api/v1/commands/:id/stats func GetCommandItemsStats(c *gin.Context) { database := c.MustGet("database").(*db.Database) commandID, err := strconv.Atoi(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "ID invalide"}) return } log.Printf("📊 [STATS] Calcul stats: cmd %d", commandID) items, err := database.GetCommandItems(commandID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if len(items) == 0 { c.JSON(http.StatusOK, gin.H{ "success": true, "items_count": 0, "total_items": 0, "total_price": 0, "avg_price": 0, }) return } totalItems := 0 totalPrice := 0.0 statusCounts := make(map[string]int) for _, item := range items { quantite := int(item["quantite"].(float64)) prix := item["prix"].(float64) status := item["status"].(string) totalItems += quantite totalPrice += prix * float64(quantite) statusCounts[status]++ } avgPrice := 0.0 if len(items) > 0 { avgPrice = totalPrice / float64(len(items)) } log.Printf("✅ [STATS] Items=%d, Total=%.2f€", totalItems, totalPrice) c.JSON(http.StatusOK, gin.H{ "success": true, "command_id": commandID, "items_count": len(items), "total_items": totalItems, "total_price": totalPrice, "avg_price": avgPrice, "status_breakdown": statusCounts, }) }