Files
projet_gestion_commande/backend/gestion/handlers/commands.go
T
Xor290 b8fddc51c2
Backend - Build & Lint / build (push) Failing after 31m13s
Frontend Web - Build & Lint / build (push) Failing after 9m49s
chore: build
2026-08-16 13:40:55 +02:00

1217 lines
34 KiB
Go

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),
})
}
// UpdateOwnCommandAddress permet à un client de corriger l'adresse de sa
// propre commande (ex: suite à un échec de géocodage bloquant l'assignation
// auto). Refusé si la commande est déjà en_route ou terminée (voir requête
// SQL dans db.UpdateOwnCommandAddress).
func UpdateOwnCommandAddress(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
geoService := c.MustGet("geoService").(*services.GeoService)
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
}
rateLimitKey := fmt.Sprintf("update_own_addr:%s", clientUsername)
if !checkRateLimit(rateLimitKey) {
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 !geoService.IsValidAddress(req.DeliveryAddress) {
c.JSON(http.StatusBadRequest, gin.H{"error": "Adresse introuvable, vérifiez l'orthographe ou le code postal"})
return
}
if err := database.UpdateOwnCommandAddress(commandID, clientUsername, req.DeliveryAddress); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
log.Printf("✅ [UPD_OWN_ADDR] Commande %d mise à jour par %s", commandID, clientUsername)
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Adresse mise à jour",
})
}
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,
})
}