1166 lines
32 KiB
Go
1166 lines
32 KiB
Go
// ============================================
|
|
// 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
|
|
}
|
|
|
|
func sanitizeForLog(input string) string {
|
|
// Limiter la longueur
|
|
if len(input) > 100 {
|
|
input = input[:100] + "..."
|
|
}
|
|
// Supprimer caractères de contrôle
|
|
return strings.Map(func(r rune) rune {
|
|
if r < 32 || r == 127 {
|
|
return -1
|
|
}
|
|
return r
|
|
}, input)
|
|
}
|
|
|
|
// 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,
|
|
})
|
|
}
|