package handlers import ( "bytes" "encoding/csv" "encoding/json" "fmt" "gestion/db" "gestion/services" "gestion/utils" "log" "net/http" "slices" "strconv" "strings" "sync" "time" "github.com/gin-gonic/gin" ) var ( rateLimitMap = make(map[string][]time.Time) rateLimitMu sync.Mutex maxRequests = 10 timeWindow = time.Minute ) func checkRateLimit(key string) bool { rateLimitMu.Lock() defer rateLimitMu.Unlock() now := time.Now() if timestamps, exists := rateLimitMap[key]; exists { 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 } 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 } func UpdateCommandAddress(c *gin.Context) { database := c.MustGet("database").(*db.Database) userRole := c.GetString("role") if !utils.CheckRoleAdmin(c, userRole) { 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": "Non authentifié"}) return } 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 } if err := validateAddress(req.DeliveryAddress); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } 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, }) } // ProposeAddressChange propose une nouvelle adresse au client pour validation func ProposeAddressChange(c *gin.Context) { database := c.MustGet("database").(*db.Database) userRole := c.GetString("role") if !utils.CheckRoleAdmin(c, userRole) && !utils.CheckRoleCabine(c, userRole) { c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"}) return } staffUsername, err := safeGetUsername(c) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"}) 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 { ProposedAddress string `json:"proposed_address" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"}) return } if err := validateAddress(req.ProposedAddress); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } 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.ProposeAddressChange(commandID, req.ProposedAddress, staffUsername); err != nil { utils.ServerErr(c, "Impossible de proposer l'adresse", err) return } clientUsername, _ := command["username"].(string) if clientUsername != "" { msg := fmt.Sprintf("📍 Une nouvelle adresse de livraison vous est proposée pour votre commande #%d : %s. Veuillez l'accepter ou la refuser dans le suivi de commande.", database.GetClientOrderID(commandID), req.ProposedAddress) database.NotifyClient(clientUsername, commandID, "address_proposal", msg) } c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Nouvelle adresse proposée au client", "command_id": commandID, }) } // RespondToAddressProposal permet au client d'accepter ou refuser une proposition d'adresse // POST /api/v1/commands/:id/address/respond func RespondToAddressProposal(c *gin.Context) { database := c.MustGet("database").(*db.Database) userRole := c.GetString("role") if !utils.CheckRoleClient(c, userRole) { c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"}) return } clientUsername, err := safeGetUsername(c) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentification requise"}) 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 { Accepted bool `json:"accepted"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"}) return } if err := database.RespondToAddressProposal(commandID, clientUsername, req.Accepted); err != nil { utils.ServerErr(c, "Impossible de traiter la réponse", err) return } action := "refusée" if req.Accepted { action = "acceptée" } log.Printf("✅ [RESPOND_ADDR] Commande %d - proposition %s par %s", commandID, action, clientUsername) c.JSON(http.StatusOK, gin.H{ "success": true, "message": fmt.Sprintf("Proposition d'adresse %s", action), }) } func ExportApprovedCommandsCSV(c *gin.Context) { database := c.MustGet("database").(*db.Database) commands, err := database.GetApprovedCommands() if err != nil { log.Printf("❌ [EXPORT_CSV] Erreur: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur export CSV"}) return } var buf bytes.Buffer w := csv.NewWriter(&buf) _ = w.Write([]string{ "ID", "Client Order ID", "User ID", "Username", "Statut", "Total (€)", "Adresse", "Livreur", "Créé le", "Mis à jour le", }) for _, cmd := range commands { _ = w.Write([]string{ strconv.Itoa(cmd.ID), strconv.Itoa(cmd.ClientOrderID), strconv.Itoa(cmd.UserID), cmd.Username, cmd.Status, strconv.FormatFloat(cmd.Total, 'f', 2, 64), cmd.DeliveryAddress, cmd.LivreurAssign, cmd.CreatedAt.Format("2006-01-02 15:04:05"), cmd.UpdatedAt.Format("2006-01-02 15:04:05"), }) } w.Flush() filename := fmt.Sprintf("commandes_approved_%s.csv", time.Now().Format("2006-01-02")) c.Header("Content-Type", "text/csv; charset=utf-8") c.Header("Content-Disposition", "attachment; filename="+filename) c.String(http.StatusOK, buf.String()) } func GetAllCommands(c *gin.Context) { database := c.MustGet("database").(*db.Database) status := c.Query("status") username := c.Query("username") userRole := c.GetString("role") if !utils.CheckRoleAdmin(c, userRole) && !utils.CheckRoleCabine(c, 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 } } 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", }) return } countCommand := len(commands) c.JSON(http.StatusOK, gin.H{ "success": true, "commands": commands, "count": countCommand, }) } // GetCommandByID récupère une commande complète avec logs 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 } cmdUsername, _ := command["username"].(string) 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 } 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 } 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) totalPoints, pointCategory, err := database.ApproveDeliveryAtomic(commandID, username) if err != nil { log.Printf("❌ [APPROVE] Erreur: %v", err) c.JSON(http.StatusBadRequest, gin.H{ "error": "Impossible d'approuver la livraison", }) return } log.Printf("✅ [APPROVE] %d points attribués à %s (catégorie: %s)", totalPoints, username, pointCategory) c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Livraison confirmée", "command_id": commandID, "points_earned": totalPoints, "category": pointCategory, }) } // ============================================ // CONFIRMATION RÉCEPTION PAR STAFF (ADMIN / CABINE) // ============================================ func StaffApproveDelivery(c *gin.Context) { database := c.MustGet("database").(*db.Database) role := c.GetString("role") if role != "admin" && role != "cabine" { c.JSON(http.StatusForbidden, gin.H{"error": "Accès réservé aux admins et à la cabine"}) return } staffUsername, err := safeGetUsername(c) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentification requise"}) 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("✅ [STAFF_APPROVE] %s (%s) confirme réception cmd %d", staffUsername, role, commandID) totalPoints, pointCategory, clientUsername, err := database.ApproveDeliveryAtomicByStaff(commandID, staffUsername) if err != nil { log.Printf("❌ [STAFF_APPROVE] Erreur: %v", err) utils.ServerErr(c, "Impossible de confirmer la réception", err) return } log.Printf("✅ [STAFF_APPROVE] %d points attribués au client %s (catégorie: %s)", totalPoints, clientUsername, pointCategory) c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Réception confirmée", "command_id": commandID, "client_username": clientUsername, "points_earned": totalPoints, "category": pointCategory, }) } 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": "Non authentifié"}) 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", "livre"} if !slices.Contains(validStatuses, currentStatus) { 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, }) } // 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", }) 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 // Admin: POST /api/v2/admin/protected/delivery-persons/:username/assign/:command_id // Cabine: POST /api/v1/cabine/commands/:id/assign (body: {"livreur_username": "..."}) func AssignDeliveryPerson(c *gin.Context) { database := c.MustGet("database").(*db.Database) geoService := c.MustGet("geoService").(*services.GeoService) // ✅ SÉCURITÉ: Admin ou Cabine role := c.GetString("role") if role != "admin" && role != "cabine" { c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"}) return } // Support deux formats de route: :command_id (admin) ou :id (cabine) commandIDStr := c.Param("command_id") if commandIDStr == "" { commandIDStr = c.Param("id") } commandID, err := strconv.Atoi(commandIDStr) if err != nil || commandID == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "ID de commande invalide"}) return } // Livreur depuis URL param (admin) ou body JSON (cabine) livreurUsername := c.Param("username") if livreurUsername == "" { 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", }) return } livreurUsername = req.LivreurUsername } log.Printf("👤 [ASSIGN] Assignation cmd %d à livreur %s", commandID, livreurUsername) staffUsername, _ := c.Get("username") if err := database.AssignDeliveryPerson(commandID, livreurUsername); err != nil { log.Printf("❌ [ASSIGN] Erreur: %v", err) c.JSON(http.StatusInternalServerError, gin.H{ "error": "Erreur assignation livreur", }) return } database.AddCommandLog(commandID, "assigned", fmt.Sprintf("Livreur '%s' assigné manuellement par %s", livreurUsername, staffUsername), staffUsername.(string)) // Géocodage async : stocker les coords si absentes go func() { cmd, err := database.GetCommandByID(commandID) if err != nil { return } dLat, _ := cmd["dest_latitude"].(float64) dLon, _ := cmd["dest_longitude"].(float64) if dLat != 0 && dLon != 0 { return // coords déjà présentes } adresse, _ := cmd["adresse"].(string) if adresse == "" { return } location, err := geoService.GeocodeAddress(adresse) if err != nil || location == nil { log.Printf("⚠️ [ASSIGN] Géocodage échoué pour cmd %d: %v", commandID, err) return } coordsJSON, _ := json.Marshal(map[string]float64{ "lat": location.Latitude, "lon": location.Longitude, }) destKey := fmt.Sprintf("command:destination:%d", commandID) db.Redis.Set(db.RedisCtx, destKey, coordsJSON, 4*time.Hour) database.GDB.Exec( "UPDATE commandes SET dest_latitude = ?, dest_longitude = ? WHERE id = ?", location.Latitude, location.Longitude, commandID, ) log.Printf("📍 [ASSIGN] Coords stockées pour cmd %d: (%.6f, %.6f)", commandID, location.Latitude, location.Longitude) }() log.Printf("✅ [ASSIGN] Commande %d assignée à %s", commandID, livreurUsername) c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Livreur assigné avec succès", "command_id": commandID, "livreur": livreurUsername, "assigned_by": staffUsername, }) } 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", }) 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", client.Username) } resp := gin.H{ "success": true, "commands": commands, "count": len(commands), } if err == nil && client != nil { // Récupérer les pools configurés par l'admin poolNames := []string{} poolKeys := []string{} if settings, sErr := database.GetSettings(); sErr == nil && len(settings.PointsPools) > 0 { for _, p := range settings.PointsPools { poolNames = append(poolNames, p.Name) poolKeys = append(poolKeys, p.Key) } } // Construire pool_points depuis points_extra[pool.Key] (stockage dynamique) poolPoints := make([]int, len(poolKeys)) for i, key := range poolKeys { if key != "" { poolPoints[i] = client.PointsExtra[key] } } resp["client_stats"] = gin.H{ "username": client.Username, "nom": client.Nom, "prenom": client.Prenom, "telephone": client.Telephone, "total_commands": client.Command, "pool_points": poolPoints, "pool_names": poolNames, "penalties": client.Amende, } log.Printf("📊 [HISTORY] pool_names=%v pool_keys=%v pool_points=%v extra=%v", poolNames, poolKeys, poolPoints, client.PointsExtra) } 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) } func NotifyClientToDescend(c *gin.Context) { database := c.MustGet("database").(*db.Database) role := c.GetString("role") if role != "admin" && role != "cabine" { c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"}) 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 } cmd, err := database.GetCommandByID(commandID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Commande introuvable"}) return } clientUsername, ok := cmd["username"].(string) if !ok || clientUsername == "" { c.JSON(http.StatusInternalServerError, gin.H{"error": "Client introuvable"}) return } staffUsername, _ := c.Get("username") msg := fmt.Sprintf("Votre commande #%d est prête ! Vous pouvez descendre la récupérer.", database.GetClientOrderID(commandID)) database.NotifyClient(clientUsername, commandID, "ready_pickup", msg) database.AddCommandLog(commandID, "notification", fmt.Sprintf("Client notifié de descendre par %s", staffUsername), staffUsername.(string)) log.Printf("🔔 [NOTIFY] Client %s notifié pour commande %d par %s", clientUsername, commandID, staffUsername) c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Client notifié", "client_username": clientUsername, }) } // ShowItems affiche les items d'une commande (Admin/Cabine) func ShowItems(c *gin.Context) { database := c.MustGet("database").(*db.Database) userRole := c.GetString("role") 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", }) return } if len(items) == 0 { log.Printf("⚠️ [ITEMS] Aucun item trouvé") c.JSON(http.StatusOK, gin.H{ "success": true, "items": []map[string]any{}, "count": 0, }) return } log.Printf("✅ [ITEMS] %d items récupérés", len(items)) commandInfo := map[string]any{ "id": items[0]["command_id"], "status": items[0]["command_status"], "address": items[0]["command_address"], "total_prix": items[0]["total_prix"], "referral_used": items[0]["referral_used"], "livreur": items[0]["livreur_assign"], "created_at": items[0]["command_created_at"], } clientInfo := map[string]any{ "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 func GetCommandItemsWithDetails(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 } 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) 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 } 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", }) return } if len(items) == 0 { c.JSON(http.StatusNotFound, gin.H{ "error": "Aucun item trouvé pour cette commande", }) return } commandInfo := map[string]any{ "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"], "client_order_number": items[0]["client_order_number"], } 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"], }, }) } 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", }) return } log.Printf("📝 [UPD_ITEM] Mise à jour: item=%d, status=%s", itemID, req.Status) validStatuses := []string{"pending", "preparing", "delivered"} if !slices.Contains(validStatuses, req.Status) { 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", }) 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, }) } // DELETE /api/v2/admin/protected/orders/:id/items/:item_id func DeleteCommandItem(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": "Non authentifié"}) 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 } itemID, err := strconv.Atoi(c.Param("item_id")) if err != nil || itemID <= 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "ID d'item invalide"}) return } log.Printf("🗑️ [DEL_ITEM] Admin %s supprime item %d de cmd %d", adminUsername, itemID, commandID) if err := database.DeleteCommandItem(commandID, itemID); err != nil { utils.ServerErr(c, "Impossible de supprimer l'item", err) return } database.AddCommandLog(commandID, "item_deleted", fmt.Sprintf("Item %d supprimé par admin %s", itemID, adminUsername), adminUsername) log.Printf("✅ [DEL_ITEM] Item %d supprimé", itemID) c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Item supprimé", "command_id": commandID, "item_id": itemID, }) } // PUT /api/v2/admin/protected/orders/:id/status func UpdateCommandStatusAdmin(c *gin.Context) { database := c.MustGet("database").(*db.Database) role := c.GetString("role") if role != "admin" && role != "cabine" { c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"}) 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 { Status string `json:"status" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Statut manquant"}) return } allowed := map[string]bool{ "pending": true, "assigned": true, "en_route": true, "arrived": true, "livre": true, "approved": true, "cancelled": true, } if !allowed[req.Status] { c.JSON(http.StatusBadRequest, gin.H{"error": "Statut invalide: " + req.Status}) return } if req.Status == "cancelled" { if err := database.CancelCommandByAdminAtomic(commandID); err != nil { utils.ServerErr(c, "Impossible d'annuler la commande", err) return } } else if err := database.UpdateCommandStatus(commandID, req.Status); err != nil { utils.ServerErr(c, "Impossible de mettre à jour le statut", err) return } log.Printf("✅ [STATUS_ADMIN] Cmd %d → %s (par %s)", commandID, req.Status, role) c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Statut mis à jour", "command_id": commandID, "new_status": req.Status, }) }