chore: refacto

This commit is contained in:
2026-05-14 19:33:39 +02:00
parent e0c354d76c
commit dce417c209
19 changed files with 63 additions and 1435 deletions
+3 -81
View File
@@ -412,9 +412,9 @@ func GetClient2FAStatus(c *gin.Context) {
settings, _ := database.GetSettings()
c.JSON(http.StatusOK, gin.H{
"two_fa_enabled": client.TwoFAEnabled,
"telegram_linked": tgLinked,
"admin_2fa_enabled": settings.Telegram2FAEnabled,
"two_fa_enabled": client.TwoFAEnabled,
"telegram_linked": tgLinked,
"admin_2fa_enabled": settings.Telegram2FAEnabled,
})
}
@@ -520,60 +520,6 @@ func LogoutClient(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Déconnexion réussie"})
}
// RegisterAdmin crée un nouvel utilisateur admin/cabine/livreur
func RegisterAdmin(c *gin.Context) {
var req models.RegisterAdminRequest
if err := c.ShouldBindJSON(&req); err != nil {
log.Printf("❌ [REGISTER_ADMIN] Erreur binding: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"})
return
}
database := c.MustGet("database").(*db.Database)
if existingUser, _ := database.GetUserByUsername(req.Username); existingUser != nil {
log.Printf("❌ [REGISTER_ADMIN] Username déjà utilisé: %s", req.Username)
c.JSON(http.StatusConflict, gin.H{"error": "Nom d'utilisateur déjà utilisé"})
return
}
hashed, _ := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
user := &models.User{
Username: req.Username,
Password: string(hashed),
Role: req.Role,
}
if err := database.CreateUser(user); err != nil {
log.Printf("❌ [REGISTER_ADMIN] Erreur création: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur création utilisateur"})
return
}
token, err := generateAdminToken(user)
if err != nil {
log.Printf("❌ [REGISTER_ADMIN] Erreur génération token: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur génération token"})
return
}
expiresAt := time.Now().Add(adminTokenDuration)
if err := database.SaveToken(user.ID, user.Role, token, expiresAt); err != nil {
log.Printf("❌ [REGISTER_ADMIN] Erreur SaveToken: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur enregistrement token"})
return
}
user.Password = ""
c.JSON(http.StatusCreated, models.LoginResponse{
AccessToken: token,
TokenType: "Bearer",
ExpiresIn: int(adminTokenDuration.Seconds()),
User: user,
})
}
// LoginAdmin authentifie un admin/cabine/livreur
func LoginAdmin(c *gin.Context) {
var req models.LoginRequest
@@ -707,30 +653,6 @@ func GetCurrentAdmin(c *gin.Context) {
})
}
// HealthCheck vérifie la santé de l'API
// GET /api/v1/health
func HealthCheck(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
if err := database.DB.Ping(); err != nil {
log.Printf("⚠️ [HEALTH] Database down: %v", err)
c.JSON(http.StatusServiceUnavailable, gin.H{
"status": "unhealthy",
"database": "disconnected",
"timestamp": time.Now().Unix(),
})
return
}
log.Printf("✅ [HEALTH] API healthy")
c.JSON(http.StatusOK, gin.H{
"status": "healthy",
"database": "connected",
"timestamp": time.Now().Unix(),
"version": "2.0.0",
})
}
// GetAllUsers récupère tous les utilisateurs (Admin only)
func GetAllUsers(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
+4 -508
View File
@@ -1,244 +1,13 @@
// ============================================
// handlers/cabine_handlers.go - COMPLET
// INCLUT: SetCommandDestinationCoordinates
// ============================================
package handlers
import (
"encoding/json"
"fmt"
"gestion/db"
"gestion/utils"
"log"
"net/http"
"slices"
"strconv"
"time"
"github.com/gin-gonic/gin"
)
// ============================================
// 0️⃣ FONCTION ADMIN: SET DESTINATION COORDINATES
// ============================================
// SetCommandDestinationCoordinates stocke les coordonnées destination en Redis
func SetCommandDestinationCoordinates(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
userRole := c.GetString("role")
if userRole != "admin" {
c.JSON(http.StatusForbidden, gin.H{"error": "Accès réservé aux administrateurs"})
return
}
adminUsername := c.GetString("username")
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 {
Latitude float64 `json:"latitude" binding:"required"`
Longitude float64 `json:"longitude" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Latitude et longitude requises",
})
return
}
// Validation des coordonnées GPS
if req.Latitude < -90 || req.Latitude > 90 {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Latitude invalide (doit être entre -90 et 90)",
"value": req.Latitude,
})
return
}
if req.Longitude < -180 || req.Longitude > 180 {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Longitude invalide (doit être entre -180 et 180)",
"value": req.Longitude,
})
return
}
if !utils.CheckCommand(commandID, database) {
c.JSON(http.StatusNotFound, gin.H{"error": "Commande non trouvée"})
}
destCacheKey := fmt.Sprintf("command:destination:%d", commandID)
coordsJSON, _ := json.Marshal(map[string]float64{
"lat": req.Latitude,
"lon": req.Longitude,
})
ttlSeconds := 24 * 60 * 60 // 24 heures
err = db.Redis.Set(db.RedisCtx, destCacheKey, coordsJSON, time.Duration(ttlSeconds)*time.Second).Err()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur stockage Redis",
})
return
}
// Ajouter un log
database.AddCommandLog(commandID, "destination_set",
fmt.Sprintf("Coordonnées destination définies par admin %s: (%.6f, %.6f) via Redis",
adminUsername, req.Latitude, req.Longitude),
adminUsername)
log.Printf("✅ [ADMIN %s] Coordonnées destination définies pour CMD %d: (%.6f, %.6f) en Redis",
adminUsername, commandID, req.Latitude, req.Longitude)
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Coordonnées définies avec succès en Redis",
"command_id": commandID,
})
}
// ============================================
// 1. CLIENT PROFILE
// ============================================
func GetClientProfile(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
username := c.Param("username")
if username == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Username requis"})
return
}
client, err := database.GetClientByUsername(username)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Client non trouvé"})
return
}
client.Password = ""
c.JSON(http.StatusOK, gin.H{
"success": true,
"client": gin.H{
"id": client.ID,
"username": client.Username,
"command": client.Command,
"amende": client.Amende,
"points_extra": client.PointsExtra,
"created_at": client.CreatedAt,
},
})
}
func GetClientFullHistory(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
username := c.Param("username")
if username == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Username requis"})
return
}
client, err := database.GetClientByUsername(username)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Client non trouvé"})
return
}
commands, err := database.GetAllCommands("", username)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur récupération historique"})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"client": gin.H{
"username": client.Username,
"total_commands": client.Command,
"amende": client.Amende,
"points_extra": client.PointsExtra,
},
"commands": commands,
"count": len(commands),
})
}
// ============================================
// 2. UPDATE ADDRESS
// ============================================
func UpdateCommandAddressCabine(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 de commande invalide"})
return
}
var req struct {
DeliveryAddress string `json:"delivery_address" binding:"required"`
Reason string `json:"reason"`
}
if err := c.ShouldBindJSON(&req); err != nil || req.DeliveryAddress == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Adresse de livraison requise"})
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)
allowedStatuses := []string{"pending", "", "assigned"}
if !slices.Contains(allowedStatuses, status) {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Impossible de modifier l'adresse d'une commande en cours ou terminée",
"current_status": status,
"allowed_statuses": allowedStatuses,
})
return
}
if err := database.UpdateCommandAddress(commandID, req.DeliveryAddress); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur lors de la mise à jour de l'adresse",
})
return
}
cabineUsername, _ := c.Get("username")
message := fmt.Sprintf("Adresse modifiée par cabine: %s", req.DeliveryAddress)
if req.Reason != "" {
message += fmt.Sprintf(" (Raison: %s)", req.Reason)
}
database.AddCommandLog(commandID, "address_updated", message, cabineUsername.(string))
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Adresse de livraison mise à jour",
"command_id": commandID,
"delivery_address": req.DeliveryAddress,
})
}
// ============================================
// 3. LIVREUR POSITION
// ============================================
func GetLivreurPosition(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
livreurUsername := c.Param("username")
@@ -273,108 +42,6 @@ func GetLivreurPosition(c *gin.Context) {
})
}
func GetDeliveryTrackingClient(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
}
commandID, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "ID de commande invalide"})
return
}
command, err := database.GetCommandByID(commandID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Commande non trouvée"})
return
}
if command["username"].(string) != username.(string) {
c.JSON(http.StatusForbidden, gin.H{"error": "Cette commande ne vous appartient pas"})
return
}
livreurAssign, _ := command["livreur_assign"].(string)
logs, _ := database.GetCommandLogs(commandID)
c.JSON(http.StatusOK, gin.H{
"success": true,
"command_id": commandID,
"status": command["status"],
"livreur": livreurAssign,
"address": command["adresse"],
"logs": logs,
"message": "Suivi en cours - ETA disponible via /api/v1/orders/:id/eta",
})
}
// ============================================
// 5. DELIVERY TRACKING ADMIN (AVEC GPS)
// ============================================
func GetDeliveryTracking(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
userRole := c.GetString("role")
if userRole != "admin" && userRole != "cabine" {
c.JSON(http.StatusForbidden, gin.H{
"error": "Accès refusé - Réservé aux administrateurs et cabines",
})
return
}
commandID, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "ID de commande invalide"})
return
}
command, err := database.GetCommandByID(commandID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Commande non trouvée"})
return
}
livreurAssign, _ := command["livreur_assign"].(string)
if livreurAssign == "" {
c.JSON(http.StatusOK, gin.H{
"success": true,
"command": command,
"status": "Aucun livreur assigné",
})
return
}
position, err := database.GetLivreurPosition(livreurAssign)
logs, _ := database.GetCommandLogs(commandID)
response := gin.H{
"success": true,
"command": command,
"livreur": livreurAssign,
"logs": logs,
}
status, _ := command["status"].(string)
if err != nil && (status == "livre" || status == "approved") {
response["livreur_position"] = nil
response["position_status"] = "Livraison terminée - Position non suivie"
} else if err != nil {
response["livreur_position"] = nil
response["position_status"] = "Position non disponible (GPS peut-être désactivé)"
} else {
response["livreur_position"] = position
response["position_status"] = "Position en temps réel"
}
c.JSON(http.StatusOK, response)
}
func GetDeliveryIssues(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
@@ -390,7 +57,7 @@ func GetDeliveryIssues(c *gin.Context) {
issues, err := database.GetDeliveryIssues(status)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur récupération problèmes",
"error": "Erreur récupération problèmes",
})
return
}
@@ -426,7 +93,7 @@ func CreateDeliveryIssue(c *gin.Context) {
)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur création problème",
"error": "Erreur création problème",
})
return
}
@@ -462,7 +129,7 @@ func UpdateDeliveryIssue(c *gin.Context) {
err = database.UpdateDeliveryIssue(issueID, req.Status, req.Resolution, cabineUsername.(string))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur mise à jour",
"error": "Erreur mise à jour",
})
return
}
@@ -473,53 +140,6 @@ func UpdateDeliveryIssue(c *gin.Context) {
})
}
func AddDeliverySupport(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 commande invalide"})
return
}
var req struct {
Message string `json:"message"`
}
c.ShouldBindJSON(&req)
if req.Message == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Message requis",
"example": gin.H{
"message": "Votre message de support ici",
},
})
return
}
cabineUsername, _ := c.Get("username")
err = database.AddCommandLog(
commandID,
"note",
fmt.Sprintf("Note cabine: %s", req.Message),
cabineUsername.(string),
)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur ajout support",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Support ajouté",
})
}
func GetCommandLogs(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
@@ -532,7 +152,7 @@ func GetCommandLogs(c *gin.Context) {
logs, err := database.GetCommandLogs(commandID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur récupération logs",
"error": "Erreur récupération logs",
})
return
}
@@ -543,127 +163,3 @@ func GetCommandLogs(c *gin.Context) {
"count": len(logs),
})
}
// ============================================
// 7. FORCE VALIDATE DELIVERY
// ============================================
func ForceValidateDelivery(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
userRole := c.GetString("role")
if userRole != "admin" {
c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé - Admin seulement"})
return
}
adminUsername, _ := c.Get("username")
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 {
Reason string `json:"reason" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Raison requise pour validation forcée",
"example": gin.H{
"reason": "Client confirmé par téléphone",
},
})
return
}
if req.Reason == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Veuillez fournir une raison pour la validation forcée",
})
return
}
command, err := database.GetCommandByID(commandID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"error": "Commande non trouvée",
"command_id": commandID,
})
return
}
status, ok := command["status"].(string)
if !ok {
c.JSON(http.StatusBadRequest, gin.H{"error": "Statut de commande invalide"})
return
}
if status == "livre" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Cette commande a déjà été validée",
"current_status": status,
})
return
}
validStatuses := []string{"assigned", "en_route", "pending", "priority"}
if !slices.Contains(validStatuses, status) {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Commande ne peut pas être validée de force dans ce statut",
"current_status": status,
"valid_statuses": validStatuses,
})
return
}
err = database.UpdateCommandStatus(commandID, "livre")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur lors de la validation forcée",
})
return
}
clientUsername, _ := command["username"].(string)
livreurAssign, _ := command["livreur_assign"].(string)
if clientUsername != "" {
clientMsg := fmt.Sprintf("Ta commande #%d a bien été livrée ! Bonne dégustation l'ami et à bientôt 😊\n\n<b>⚠️ VALIDE LA RÉCEPTION DE TA COMMANDE DANS LA RUBRIQUE SUIVI POUR RÉCUPÉRER TES POINTS DE FIDÉLITÉ ⚠️</b>", database.GetClientOrderID(commandID))
database.NotifyClient(clientUsername, commandID, "livre", clientMsg)
}
if err := database.IncrementClientCommandCount(clientUsername); err != nil {
log.Printf("⚠️ Erreur compteur commandes: %v", err)
}
if err := database.AddClientPointsByCategory(clientUsername, 10, ""); err != nil {
log.Printf("⚠️ Erreur ajout points: %v", err)
}
if livreurAssign != "" {
err := database.CompleteDeliveryAndProcessNext(livreurAssign, commandID)
if err != nil {
log.Printf("⚠️ Erreur optimisation: %v", err)
}
}
message := fmt.Sprintf("VALIDATION FORCÉE par admin %s - Raison: %s", adminUsername.(string), req.Reason)
database.AddCommandLog(commandID, "livre", message, adminUsername.(string))
log.Printf("🔴 Commande %d validée de force par %s - Raison: %s", commandID, adminUsername.(string), req.Reason)
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Livraison validée de force (sans vérification GPS)",
"command_id": commandID,
"validation_type": "forced",
"reason": req.Reason,
"validated_by": adminUsername.(string),
"new_status": "livre",
"points_awarded": 10,
"queue_optimized": livreurAssign != "",
})
}
@@ -1,9 +1,3 @@
// ============================================
// handlers/cancel_command_handler.go
// ANNULATION DE COMMANDES AVEC SANCTIONS ÉVOLUTIVES
// VERSION SÉCURISÉE - FIX ETA CHECK
// ============================================
package handlers
import (
@@ -218,9 +212,6 @@ func CancelCommandByClient(c *gin.Context) {
return
}
// ============================================
// SUCCÈS
// ============================================
log.Printf("✅ [CANCEL_CLIENT] Commande %d annulée", commandID)
response := gin.H{
@@ -242,10 +233,6 @@ func CancelCommandByClient(c *gin.Context) {
c.JSON(http.StatusOK, response)
}
// ============================================
// HISTORIQUE DES ANNULATIONS - VERSION SÉCURISÉE
// ============================================
func GetMyCancellationHistory(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
-11
View File
@@ -595,10 +595,6 @@ func ValidateDelivery(c *gin.Context) {
})
}
// ============================================
// GESTION ADMIN
// ============================================
// GetAvailableDeliveryPersons récupère les livreurs disponibles
// GET /api/v1/admin/delivery-persons/available
func GetAvailableDeliveryPersons(c *gin.Context) {
@@ -778,13 +774,6 @@ func GetClientCommandsHistory(c *gin.Context) {
c.JSON(http.StatusOK, resp)
}
// ============================================
// NOTIFICATIONS CLIENT
// ============================================
// NotifyClientToDescend envoie une notification push au client pour descendre récupérer sa commande
// POST /api/v2/admin/protected/orders/:id/notify-client
// POST /api/v1/cabine/commands/:id/notify-client
func NotifyClientToDescend(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
+9 -32
View File
@@ -11,6 +11,7 @@ import (
"gestion/utils"
"log"
"net/http"
"slices"
"strconv"
"time"
@@ -39,7 +40,7 @@ func GetDeliveryPersonDetails(c *gin.Context) {
if err != nil {
log.Printf("❌ [GET_DELIVERY_DETAILS] Livreur non trouvé: %v", err)
c.JSON(http.StatusNotFound, gin.H{
"error": "Livreur non trouvé",
"error": "Livreur non trouvé",
})
return
}
@@ -116,22 +117,14 @@ func UpdateDeliveryPersonStatusAdmin(c *gin.Context) {
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Statut requis",
"error": "Statut requis",
})
return
}
// Valider le statut
validStatuses := []string{"available", "busy", "offline"}
isValid := false
for _, vs := range validStatuses {
if req.Status == vs {
isValid = true
break
}
}
if !isValid {
if !slices.Contains(validStatuses, req.Status) {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Statut invalide",
"valid_statuses": validStatuses,
@@ -160,7 +153,7 @@ func UpdateDeliveryPersonStatusAdmin(c *gin.Context) {
if err != nil {
log.Printf("❌ [UPDATE_DELIVERY_STATUS] Erreur mise à jour: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur mise à jour statut",
"error": "Erreur mise à jour statut",
})
return
}
@@ -321,7 +314,7 @@ func GetDeliveryPersonHistory(c *gin.Context) {
if err != nil {
log.Printf("❌ [GET_DELIVERY_HISTORY] Erreur récupération: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur récupération historique",
"error": "Erreur récupération historique",
})
return
}
@@ -368,7 +361,7 @@ func UpdateDeliveryPersonLocationAdmin(c *gin.Context) {
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Coordonnées GPS requises",
"error": "Coordonnées GPS requises",
})
return
}
@@ -415,7 +408,7 @@ func UpdateDeliveryPersonLocationAdmin(c *gin.Context) {
if err != nil {
log.Printf("❌ [UPDATE_DELIVERY_LOCATION] Erreur mise à jour: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur mise à jour position",
"error": "Erreur mise à jour position",
})
return
}
@@ -437,12 +430,6 @@ func UpdateDeliveryPersonLocationAdmin(c *gin.Context) {
})
}
// ============================================
// 🗑️ REMOVE COMMAND FROM QUEUE
// ============================================
// RemoveCommandFromQueue retire une commande de la queue d'un livreur
// DELETE /api/v2/admin/protected/delivery-persons/:username/queue/:command_id
func RemoveCommandFromQueue(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
@@ -474,9 +461,6 @@ func RemoveCommandFromQueue(c *gin.Context) {
log.Printf("🗑️ [REMOVE_FROM_QUEUE] Suppression: cmd %d de la queue de %s", commandID, username)
// ============================================
// Vérifier que le livreur existe
// ============================================
livreur, err := database.GetUserByUsername(username)
if err != nil {
log.Printf("❌ [REMOVE_FROM_QUEUE] Livreur non trouvé")
@@ -491,9 +475,6 @@ func RemoveCommandFromQueue(c *gin.Context) {
return
}
// ============================================
// Vérifier que la commande existe
// ============================================
command, err := database.GetCommandByID(commandID)
if err != nil {
log.Printf("❌ [REMOVE_FROM_QUEUE] Commande non trouvée")
@@ -501,19 +482,15 @@ func RemoveCommandFromQueue(c *gin.Context) {
return
}
// ============================================
// Retirer de la queue
// ============================================
err = database.RemoveCommandFromDeliverymanQueue(username, commandID)
if err != nil {
log.Printf("❌ [REMOVE_FROM_QUEUE] Erreur suppression: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur suppression de la queue",
"error": "Erreur suppression de la queue",
})
return
}
// Optionnel: Réassigner la commande en "pending"
currentStatus, _ := command["status"].(string)
if currentStatus == "assigned" || currentStatus == "en_route" {
err = database.UpdateCommandStatus(commandID, "pending")
-5
View File
@@ -1,8 +1,3 @@
// ============================================
// handlers/eta_handler_corrected.go
// CORRECTION: ETA visible UNIQUEMENT après en_route
// ============================================
package handlers
import (
-22
View File
@@ -13,11 +13,6 @@ import (
"github.com/gin-gonic/gin"
)
// ============================================
// GÉOCODAGE D'ADRESSES
// ============================================
// geo_handlers.go
func GeocodeAddress(c *gin.Context) {
geoService := c.MustGet("geoService").(*services.GeoService)
@@ -323,9 +318,6 @@ func AutoAssignNearestDeliveryPerson(c *gin.Context) {
log.Printf("📍 Adresse géocodée: %s -> (%.6f, %.6f)", address, location.Latitude, location.Longitude)
// ============================================
// 🔹 SAUVEGARDER LES COORDONNÉES DANS LE CACHE REDIS
// ============================================
destCacheKey := fmt.Sprintf("command:destination:%d", commandID)
coordsJSON, _ := json.Marshal(map[string]float64{
"lat": location.Latitude,
@@ -555,10 +547,6 @@ func AutoAssignNearestDeliveryPerson(c *gin.Context) {
})
}
// ============================================
// ASSIGNATION EN MASSE (TOUTES LES COMMANDES PENDING)
// ============================================
// AutoAssignAllPendingCommands assigne toutes les commandes en attente
// POST /api/v2/admin/protected/commands/auto-assign-all
func AutoAssignAllPendingCommands(c *gin.Context) {
@@ -716,10 +704,6 @@ func AutoAssignAllPendingCommands(c *gin.Context) {
})
}
// ============================================
// RÉCUPÉRER L'ÉTAT DES QUEUES DES LIVREURS
// ============================================
// GetAllDeliveryQueues retourne l'état de toutes les queues des livreurs
// GET /api/v2/admin/protected/delivery/queues
func GetAllDeliveryQueues(c *gin.Context) {
@@ -769,12 +753,6 @@ func GetAllDeliveryQueues(c *gin.Context) {
})
}
// ============================================
// RÉCUPÉRER LA QUEUE D'UN LIVREUR SPÉCIFIQUE
// ============================================
// GetDeliverymanQueue retourne la queue d'un livreur spécifique
// GET /api/v2/admin/protected/delivery/:username/queue
func GetDeliverymanQueue(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
-51
View File
@@ -13,7 +13,6 @@ import (
"net/url"
"strconv"
"github.com/gin-gonic/gin"
)
@@ -79,56 +78,6 @@ func GetDeliveryPersonMapLinks(c *gin.Context) {
})
}
// GetCommandNavigationLinks génère les liens de navigation pour une commande
// GET /api/v2/admin/protected/commands/:id/navigation-links
func GetCommandNavigationLinks(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
userRole := c.GetString("role")
if userRole != "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
}
// Récupérer la commande
command, err := database.GetCommandByID(commandID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Commande non trouvée"})
return
}
// Vérifier qu'un livreur est assigné
livreurAssign, ok := command["livreur_assign"].(string)
if !ok || livreurAssign == "" {
c.JSON(http.StatusNotFound, gin.H{
"error": "Aucun livreur assigné à cette commande",
})
return
}
// Générer les liens de navigation
links, err := database.GenerateMapLinksForCommand(commandID, livreurAssign)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur génération des liens",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"command_id": commandID,
"deliveryman": livreurAssign,
"navigation_links": links,
})
}
// GetLivreurNavLink retourne le lien Waze App pour une livraison assignée au livreur connecté
// GET /api/v1/livreur/deliveries/:id/nav-link
func GetLivreurNavLink(c *gin.Context) {
+5 -21
View File
@@ -1,8 +1,3 @@
// ============================================
// handlers/history_handlers.go
// ============================================
// Gestion de l'historique des commandes terminées
package handlers
import (
@@ -12,14 +7,9 @@ import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
// GetMyCompletedOrders récupère l'historique des commandes terminées du client
// GET /api/v1/my-commands/history
// ✅ Authentification requise (ClientMiddleware)
// ✅ Retourne uniquement les commandes avec status = "approved"
func GetMyCompletedOrders(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
@@ -41,7 +31,7 @@ func GetMyCompletedOrders(c *gin.Context) {
if err != nil {
log.Printf("❌ [HISTORY] Erreur: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur lors de la récupération de l'historique",
"error": "Erreur lors de la récupération de l'historique",
})
return
}
@@ -96,8 +86,6 @@ func GetMyCompletedOrders(c *gin.Context) {
// GetMyCompletedOrdersWithItems récupère l'historique avec les détails des items
// GET /api/v1/my-commands/history/detailed
// ✅ Authentification requise (ClientMiddleware)
// ✅ Retourne les commandes approved avec tous les items
func GetMyCompletedOrdersWithItems(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
@@ -119,13 +107,13 @@ func GetMyCompletedOrdersWithItems(c *gin.Context) {
if err != nil {
log.Printf("❌ [HISTORY_DETAILED] Erreur: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur lors de la récupération de l'historique",
"error": "Erreur lors de la récupération de l'historique",
})
return
}
// ✅ Enrichir chaque commande avec ses items
var enrichedCommands []map[string]interface{}
var enrichedCommands []map[string]any
for _, command := range commands {
commandID, _ := strconv.Atoi(fmt.Sprintf("%v", command["id"]))
@@ -137,11 +125,11 @@ func GetMyCompletedOrdersWithItems(c *gin.Context) {
items, err := database.GetCommandItems(commandID)
if err != nil {
log.Printf("⚠️ [HISTORY_DETAILED] Erreur items pour cmd %d: %v", commandID, err)
items = []map[string]interface{}{}
items = []map[string]any{}
}
// Ajouter les items à la commande
enrichedCommand := make(map[string]interface{})
enrichedCommand := make(map[string]any)
for k, v := range command {
enrichedCommand[k] = v
}
@@ -177,10 +165,6 @@ func GetMyCompletedOrdersWithItems(c *gin.Context) {
c.JSON(http.StatusOK, response)
}
// GetOrderHistory récupère l'historique d'une commande spécifique avec logs
// GET /api/v1/commands/:id/history
// ✅ Authentification requise
// ✅ Vérifie que la commande appartient au client
func GetOrderHistory(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
-24
View File
@@ -1,7 +1,3 @@
// ============================================
// handlers/basket_handlers_CORRIGES.go
// ============================================
package handlers
import (
@@ -78,10 +74,6 @@ func AddProductsBasket(c *gin.Context) {
})
}
// ============================================
// ============================================
// GET /api/v1/panier/:username
// Récupère le panier du client authentifié
func GetAllBaskets(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
username := c.Param("username")
@@ -148,11 +140,6 @@ func GetAllBaskets(c *gin.Context) {
})
}
// ============================================
// ✅ SÉCURISÉ: DeleteProductFromBasket
// ============================================
// DELETE /api/v1/panier/remove
// Supprime un produit du panier
func DeleteProductFromBasket(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
@@ -212,11 +199,6 @@ func DeleteProductFromBasket(c *gin.Context) {
})
}
// ============================================
// ✅ SÉCURISÉ: ClearBasket
// ============================================
// DELETE /api/v1/panier/clear
// Vide le panier du client
func ClearBasket(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
@@ -391,9 +373,6 @@ func ValidateBasket(c *gin.Context) {
log.Printf("✅ [CHECKOUT] Zone OK: %s, total=%.2f€ >= %.2f€, crédit parrainage utilisé: %.2f€", zoneResult.ZoneName, cartTotal, zoneResult.MinAmount, referralUsed)
// ============================================
// 2️⃣ Débiter le parrainage AVANT la commande (évite double-spend)
// ============================================
if referralUsed > 0 {
if err := database.DebitReferralBalance(usernameStr, referralUsed); err != nil {
log.Printf("❌ [CHECKOUT] Solde parrainage insuffisant pour %s: %v", usernameStr, err)
@@ -451,9 +430,6 @@ func ValidateBasket(c *gin.Context) {
log.Printf("✅ [CHECKOUT] Commande %d créée", commandID)
// ============================================
// PAIEMENT CRYPTO - créer le paiement NowPayments
// ============================================
if isCrypto {
np := c.MustGet("nowpayments").(*services.NowPaymentsClient)
ipnURL := fmt.Sprintf("%s/api/v1/webhooks/nowpayments", getBaseURL(c))
-39
View File
@@ -17,10 +17,6 @@ import (
"github.com/gin-gonic/gin"
)
// ============================================
// CONFIGURATION & LIMITES
// ============================================
const (
MaxFileSize = 10 * 1024 * 1024 // 10MB par fichier
MaxTotalUploadSize = 50 * 1024 * 1024 // 50MB total
@@ -30,7 +26,6 @@ const (
MaxProductsPerUser = 100 // Limite pour éviter spam
)
// ✅ MIME types autorisés (vérification réelle du contenu)
var allowedMimeTypes = map[string]bool{
"image/jpeg": true,
"image/png": true,
@@ -41,28 +36,6 @@ var allowedMimeTypes = map[string]bool{
"video/quicktime": true,
}
// ============================================
// MIDDLEWARE D'AUTHORIZATION
// ============================================
func RequireAdminOrCabine() gin.HandlerFunc {
return func(c *gin.Context) {
role := c.GetString("role")
if role != "admin" && role != "cabine" {
c.JSON(http.StatusForbidden, gin.H{
"error": "Accès refusé - Admin ou Cabine requis",
})
c.Abort()
return
}
c.Next()
}
}
// ============================================
// HELPERS DE VALIDATION
// ============================================
func validateProductName(name string) error {
if len(name) == 0 {
return fmt.Errorf("nom requis")
@@ -187,10 +160,6 @@ func sanitizeFilePath(path string) (string, error) {
return cleaned, nil
}
// ============================================
// CREATE PRODUCT - VERSION SÉCURISÉE
// ============================================
func CreateProduct(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
@@ -475,10 +444,6 @@ func CreateProduct(c *gin.Context) {
})
}
// ============================================
// GET ENDPOINTS - SÉCURISÉS (lecture publique OK)
// ============================================
func GetAllProducts(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
@@ -567,10 +532,6 @@ func GetProductByID(c *gin.Context) {
})
}
// ============================================
// UPDATE PRODUCT - VERSION SÉCURISÉE
// ============================================
func UpdateProduct(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
+5 -234
View File
@@ -1,8 +1,3 @@
// ============================================
// handlers/redis_handlers.go - VERSION FINALE
// UTILISE UNIQUEMENT LES MÉTHODES DB PostgreSQL
// ============================================
package handlers
import (
@@ -13,6 +8,7 @@ import (
"gestion/utils"
"log"
"net/http"
"slices"
"strconv"
"strings"
"time"
@@ -20,10 +16,6 @@ import (
"github.com/gin-gonic/gin"
)
// ============================================
// GESTION DE LA FILE DE COMMANDES
// ============================================
func validatePenaltyPoints(points int) error {
if points <= 0 {
return fmt.Errorf("points invalides: %d (doit être > 0)", points)
@@ -47,72 +39,6 @@ func sanitizeReason(reason string) string {
return strings.TrimSpace(reason)
}
// GetCommandQueue récupère toutes les commandes en attente dans la file Redis
// GET /api/v2/admin/protected/queue/pending
func GetCommandQueue(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
userRole := c.GetString("role")
if userRole != "admin" {
c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"})
return
}
nextCommand, err := database.GetNextCommandInQueue()
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Aucune commande en attente",
"queue": []interface{}{},
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"next_command": nextCommand,
})
}
// AutoAssignNextCommand assigne automatiquement la prochaine commande en file
// POST /api/v2/admin/protected/queue/auto-assign
func AutoAssignNextCommand(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
userRole := c.GetString("role")
if userRole != "admin" {
c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"})
return
}
nextCommand, err := database.GetNextCommandInQueue()
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"error": "Aucune commande en attente",
})
return
}
err = database.AutoAssignCommand(nextCommand.CommandID)
if err != nil {
utils.ServerErr(c, "Erreur lors de l'assignation automatique", err)
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Commande assignée automatiquement",
"command_id": nextCommand.CommandID,
})
}
// ============================================
// GESTION DES LIVREURS - LOCALISATION
// ============================================
// UpdateLivreurLocation met à jour la position GPS du livreur
// POST /api/v1/livreur/location/update
// Body: {"latitude": 48.8566, "longitude": 2.3522}
func UpdateLivreurLocation(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
@@ -171,7 +97,7 @@ func UpdateLivreurLocation(c *gin.Context) {
usernameStr, req.Latitude, req.Longitude)
// ✅ Recalculer l'ETA en temps réel si livreur en_route
go refreshETAForActivDelivery(database, usernameStr, req.Latitude, req.Longitude)
go refreshETAForActivDelivery(usernameStr, req.Latitude, req.Longitude)
// ✅ 2. Vérifier/Initialiser le statut du livreur
statusKey := fmt.Sprintf("delivery:status:%s", usernameStr)
@@ -290,14 +216,6 @@ func GetDeliveryPersonLocation(c *gin.Context) {
})
}
// ============================================
// LOCALISATION DU LIVREUR POUR UNE COMMANDE
// ============================================
// GetDeliverymanLocationForCommand récupère la position GPS du livreur assigné à une commande
// GET /api/v2/admin/protected/commands/:id/deliveryman/location (ADMIN)
// GET /api/v1/cabine/commands/:id/deliveryman/location (CABINE)
// Accessible uniquement par les admins et la cabine
func GetDeliverymanLocationForCommand(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
@@ -460,13 +378,6 @@ func GetDeliverymanLocationForCommand(c *gin.Context) {
})
}
// ============================================
// GESTION DES LIVREURS - STATUT
// ============================================
// UpdateDeliveryPersonStatus met à jour le statut de disponibilité du livreur
// POST /api/v1/livreur/status
// Body: {"status": "available" | "busy" | "offline"}
func UpdateDeliveryPersonStatus(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
@@ -490,18 +401,9 @@ func UpdateDeliveryPersonStatus(c *gin.Context) {
utils.BindErr(c, err)
return
}
// Validation du statut
validStatuses := []string{"available", "busy", "offline"}
isValid := false
for _, s := range validStatuses {
if req.Status == s {
isValid = true
break
}
}
if !isValid {
if !slices.Contains(validStatuses, req.Status) {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Statut invalide",
"valid_statuses": validStatuses,
@@ -509,7 +411,6 @@ func UpdateDeliveryPersonStatus(c *gin.Context) {
})
return
}
usernameStr := username.(string)
err := database.SetDeliveryPersonStatus(usernameStr, req.Status, 0)
@@ -604,118 +505,6 @@ func GetMyQueue(c *gin.Context) {
})
}
// GetAvailableDeliveryPersonsRealtime récupère les livreurs disponibles depuis Redis
// GET /api/v2/admin/protected/delivery/available-realtime
func GetAvailableDeliveryPersonsRealtime(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
userRole := c.GetString("role")
if userRole != "admin" {
c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"})
return
}
livreurs, err := database.GetAvailableDeliveryPersonsRedis()
if err != nil {
utils.ServerErr(c, "Erreur lors de la récupération des livreurs", err)
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"livreurs": livreurs,
"count": len(livreurs),
})
}
// ============================================
// GESTION ETA (Estimated Time of Arrival)
// ============================================
// SetCommandETAHandler permet au livreur de définir l'ETA d'une livraison
// POST /api/v1/livreur/deliveries/:id/set-eta
// Body: {"eta_minutes": 25}
func SetCommandETAHandler(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")
if userRole != "livreur" {
c.JSON(http.StatusForbidden, gin.H{"error": "Accès réservé aux livreurs"})
return
}
commandID, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "ID de commande invalide"})
return
}
var req struct {
ETAMinutes int `json:"eta_minutes" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
utils.BindErr(c, err)
return
}
// Validation de l'ETA
if req.ETAMinutes < 1 || req.ETAMinutes > 120 {
c.JSON(http.StatusBadRequest, gin.H{
"error": "L'ETA doit être entre 1 et 120 minutes",
})
return
}
usernameStr := username.(string)
// Vérifier que la commande existe et est assignée au livreur
command, err := database.GetCommandByID(commandID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"error": "Commande non trouvée",
})
return
}
livreurAssign, ok := command["livreur_assign"].(string)
if !ok || livreurAssign != usernameStr {
c.JSON(http.StatusForbidden, gin.H{
"error": "Cette commande ne vous est pas assignée",
})
return
}
// Mettre à jour l'ETA dans Redis
err = database.SetCommandETA(commandID, req.ETAMinutes)
if err != nil {
utils.ServerErr(c, "Erreur lors de la mise à jour de l'ETA", err)
return
}
log.Printf("⏱️ ETA défini pour commande %d par %s: %d minutes", commandID, usernameStr, req.ETAMinutes)
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "ETA mis à jour avec succès",
"command_id": commandID,
"eta_minutes": req.ETAMinutes,
})
}
// ============================================
// PÉNALITÉS - UTILISE PostgreSQL
// ============================================
// ApplyClientPenalty applique une pénalité à un client (Admin seulement)
// POST /api/v2/admin/protected/penalty
// Body: {"username": "john", "points": 50, "reason": "Retard paiement"}
func ApplyClientPenalty(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
@@ -993,9 +782,6 @@ func ResetClientPenaltiesAdmin(c *gin.Context) {
})
}
// AddClientPointsAdmin ajoute des points à un client dans un pool donné (Admin/Cabine)
// POST /api/v2/admin/protected/client/:username/points/add
// Body: {"pool_key": "pool_0", "points": 10}
func AddClientPointsAdmin(c *gin.Context) {
userRole := c.GetString("role")
if userRole != "admin" && userRole != "cabine" {
@@ -1040,7 +826,7 @@ func AddClientPointsAdmin(c *gin.Context) {
}
if !poolExists {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Pool de points invalide",
"error": "Pool de points invalide",
"pools_valides": func() []string {
keys := make([]string, 0, len(settings.PointsPools))
for _, p := range settings.PointsPools {
@@ -1073,9 +859,6 @@ func AddClientPointsAdmin(c *gin.Context) {
})
}
// SubtractClientPointsAdmin retire des points à un client (plancher à 0)
// POST /api/v2/admin/protected/client/:username/points/subtract
// Body: {"pool_key": "pool_0", "points": 10}
func SubtractClientPointsAdmin(c *gin.Context) {
userRole := c.GetString("role")
if userRole != "admin" && userRole != "cabine" {
@@ -1164,12 +947,6 @@ func SubtractClientPointsAdmin(c *gin.Context) {
})
}
// ============================================
// STATISTIQUES TEMPS RÉEL
// ============================================
// GetRealtimeStats récupère les statistiques en temps réel
// GET /api/v2/admin/protected/stats/realtime
func GetRealtimeStats(c *gin.Context) {
userRole := c.GetString("role")
if userRole != "admin" {
@@ -1200,13 +977,7 @@ func GetRealtimeStats(c *gin.Context) {
})
}
// ============================================
// RECALCUL ETA EN TEMPS RÉEL (appelé à chaque update GPS)
// ============================================
// refreshETAForActivDelivery recalcule l'ETA depuis la position actuelle du livreur.
// Appelé en goroutine à chaque mise à jour GPS (toutes les ~15s).
func refreshETAForActivDelivery(database *db.Database, username string, lat, lon float64) {
func refreshETAForActivDelivery(username string, lat, lon float64) {
// 1. Récupérer le statut actuel du livreur
statusKey := fmt.Sprintf("delivery:status:%s", username)
statusData, err := db.Redis.Get(db.RedisCtx, statusKey).Result()
+9 -27
View File
@@ -2,30 +2,12 @@ package handlers
import (
"gestion/db"
"gestion/models"
"net/http"
"time"
"github.com/gin-gonic/gin"
)
type weekdayRow struct {
DOW int `gorm:"column:dow"`
Count int `gorm:"column:count"`
}
type dayRow struct {
Day time.Time `gorm:"column:day"`
Count int `gorm:"column:count"`
}
type productRow struct {
ProductID int `gorm:"column:product_id"`
Name string `gorm:"column:name"`
Quantity float64 `gorm:"column:total_quantity"`
OrderCount int `gorm:"column:order_count"`
Revenue float64 `gorm:"column:revenue"`
}
var weekdayNames = []string{"Dimanche", "Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi"}
// GetAdminStats returns aggregated order & product statistics for the admin dashboard.
@@ -34,7 +16,7 @@ func GetAdminStats(c *gin.Context) {
gdb := database.GDB
// ── Commandes par jour de la semaine (all time, non annulées) ──────────────
var wdRows []weekdayRow
var wdRows []models.WeekdayRow
gdb.Raw(`
SELECT EXTRACT(DOW FROM created_at)::int AS dow, COUNT(*) AS count
FROM commandes
@@ -59,7 +41,7 @@ func GetAdminStats(c *gin.Context) {
}
// ── Commandes par jour sur 30 jours ───────────────────────────────────────
var dayRows []dayRow
var dayRows []models.DayRow
gdb.Raw(`
SELECT DATE(created_at) AS day, COUNT(*) AS count
FROM commandes
@@ -79,7 +61,7 @@ func GetAdminStats(c *gin.Context) {
}
// ── Top produits (quantité vendue, commandes terminées) ───────────────────
var prodRows []productRow
var prodRows []models.ProductRow
gdb.Raw(`
SELECT
ci.product_id,
@@ -137,11 +119,11 @@ func GetAdminStats(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"summary": gin.H{
"total_orders": totalOrders,
"total_revenue": totalRevenue,
"peak_weekday": peakWeekday,
"top_product": topProductName,
"avg_per_day": avgPerDay,
"total_orders": totalOrders,
"total_revenue": totalRevenue,
"peak_weekday": peakWeekday,
"top_product": topProductName,
"avg_per_day": avgPerDay,
},
"by_weekday": byWeekday,
"by_day_30": byDay,
+1 -1
View File
@@ -10,7 +10,7 @@ import (
)
// getFloatFromMap récupère un float64 depuis une map avec différents types
func getFloatFromMap(m map[string]interface{}, key string) (float64, bool) {
func getFloatFromMap(m map[string]any, key string) (float64, bool) {
value, exists := m[key]
if !exists || value == nil {
return 0, false
@@ -169,10 +169,6 @@ func GetMyProfile(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": true, "client": sanitizeClient(client)})
}
// ============================================
// MODIFICATION PROFIL CLIENT (PAR ADMIN)
// ============================================
// UpdateClientByAdmin permet à un admin de modifier n'importe quel profil client
// PUT /api/v2/admin/protected/clients/:id
func UpdateClientByAdmin(c *gin.Context) {
@@ -349,10 +345,6 @@ func UpdateClientByAdmin(c *gin.Context) {
})
}
// ============================================
// MODIFICATION PROFIL USER (PAR ADMIN)
// ============================================
// UpdateUserByAdmin permet à un admin de modifier n'importe quel profil user
// PUT /api/v2/admin/protected/users/:id
func UpdateUserByAdmin(c *gin.Context) {
@@ -468,10 +460,6 @@ func UpdateUserByAdmin(c *gin.Context) {
})
}
// ============================================
// UTILITAIRES
// ============================================
func sanitizeClient(client *models.Client) gin.H {
return gin.H{
"id": client.ID,
@@ -1,10 +1,8 @@
package handlers
import (
"encoding/json"
"fmt"
"gestion/db"
"gestion/services"
"log"
"net/http"
"strconv"
@@ -24,249 +22,6 @@ const (
MAX_DELIVERY_VALIDATION_DISTANCE_KM = 0.1 // 100 mètres = 0.1 km
)
// ============================================
// 1️⃣ VALIDATION LIVRAISON PAR LE LIVREUR (AVEC VÉRIFICATION GPS)
// ============================================
func ValidateDeliveryByLivreur(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
// ✅ SÉCURITÉ: Livreur seulement
username, exists := c.Get("username")
if !exists || c.GetString("role") != "livreur" {
log.Printf("❌ [VALIDATE_LIVREUR] Accès refusé")
c.JSON(http.StatusForbidden, gin.H{"error": "Accès réservé aux livreurs"})
return
}
usernameStr := username.(string)
commandID, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "ID de commande invalide"})
return
}
var req struct {
Latitude float64 `json:"latitude" binding:"required"`
Longitude float64 `json:"longitude" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
log.Printf("❌ [VALIDATE_LIVREUR] Erreur JSON: %v", err)
c.JSON(http.StatusBadRequest, gin.H{
"error": "Coordonnées GPS requises",
})
return
}
log.Printf("📍 [VALIDATE_LIVREUR] Livreur %s valide cmd %d avec GPS: (%.6f, %.6f)",
usernameStr, commandID, req.Latitude, req.Longitude)
// ✅ ÉTAPE 1: Récupérer la commande
command, err := database.GetCommandByID(commandID)
if err != nil {
log.Printf("❌ [VALIDATE_LIVREUR] Commande non trouvée")
c.JSON(http.StatusNotFound, gin.H{"error": "Commande non trouvée"})
return
}
// ✅ ÉTAPE 2: VÉRIFIER PROPRIÉTÉ
livreurAssign, _ := command["livreur_assign"].(string)
if livreurAssign != usernameStr {
log.Printf("❌ [VALIDATE_LIVREUR] ⚠️ TENTATIVE D'ACCÈS NON AUTORISÉ!")
c.JSON(http.StatusForbidden, gin.H{
"error": "Cette commande ne vous est pas assignée",
})
return
}
// ÉTAPE 3: Coordonnées GPS reçues et valides
log.Printf("📍 [VALIDATE_LIVREUR] GPS reçu: (%.6f, %.6f)", req.Latitude, req.Longitude)
// ÉTAPE 4: Sauvegarder les coordonnées du livreur
_, err = database.Exec(
"UPDATE commandes SET livreur_latitude = $1, livreur_longitude = $2 WHERE id = $3",
req.Latitude, req.Longitude, commandID,
)
if err != nil {
log.Printf("⚠️ [VALIDATE_LIVREUR] Erreur sauvegarde GPS: %v", err)
}
// ✅ ÉTAPE 5: Marquer la livraison comme "livre"
if err := database.UpdateCommandStatus(commandID, "livre"); err != nil {
log.Printf("❌ [VALIDATE_LIVREUR] Erreur update statut: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur validation",
})
return
}
// ✅ ÉTAPE 6: Ajouter un log
database.AddCommandLog(commandID, "livre",
fmt.Sprintf("Livraison confirmée par livreur - GPS: (%.6f, %.6f)", req.Latitude, req.Longitude),
usernameStr)
// ✅ ÉTAPE 7: Optimiser la queue
log.Printf("📦 [VALIDATE_LIVREUR] Optimisation queue de %s...", usernameStr)
err = database.CompleteDeliveryAndProcessNext(usernameStr, commandID)
if err != nil {
log.Printf("⚠️ [VALIDATE_LIVREUR] Erreur optimisation: %v", err)
}
log.Printf("✅ [VALIDATE_LIVREUR] Commande %d validée et marquée 'livre'", commandID)
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Livraison validée avec succès",
"command_id": commandID,
"new_status": "livre",
"gps_verified": true,
})
}
// ============================================
// 2️⃣ VÉRIFIER SI LE LIVREUR PEUT VALIDER (SANS VALIDER)
// ============================================
// CheckDeliveryValidationEligibility vérifie si le livreur peut valider une livraison
// GET /api/v1/deliveries/:id/can-validate
func CheckDeliveryValidationEligibility(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")
if userRole != "livreur" {
c.JSON(http.StatusForbidden, gin.H{"error": "Accès réservé aux livreurs"})
return
}
commandID, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "ID de commande invalide"})
return
}
command, err := database.GetCommandByID(commandID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Commande non trouvée"})
return
}
// Vérifier l'assignation
livreurAssign, _ := command["livreur_assign"].(string)
if livreurAssign != username.(string) {
c.JSON(http.StatusOK, gin.H{
"can_validate": false,
"reason": "Commande non assignée à vous",
})
return
}
// Récupérer la position du livreur
livreurLat, livreurLon, err := database.GetDeliveryPersonLocation(username.(string))
if err != nil {
c.JSON(http.StatusOK, gin.H{
"can_validate": false,
"reason": "Position GPS non disponible",
"action": "Mettez à jour votre position GPS",
})
return
}
// Récupérer les coordonnées de destination (même priorité que ValidateDeliveryByLivreur)
var destLat, destLon float64
var coordsSource string
// ✅ PRIORITÉ 1: Cache Redis
destCacheKey := fmt.Sprintf("command:destination:%d", commandID)
destData, redisErr := db.Redis.Get(db.RedisCtx, destCacheKey).Result()
if redisErr == nil && destData != "" {
var coords struct {
Lat float64 `json:"lat"`
Lon float64 `json:"lon"`
}
if err := json.Unmarshal([]byte(destData), &coords); err == nil && coords.Lat != 0 && coords.Lon != 0 {
destLat = coords.Lat
destLon = coords.Lon
coordsSource = "REDIS"
log.Printf("📍 [CAN-VALIDATE] Coords depuis Redis: (%.6f, %.6f)", destLat, destLon)
}
}
// ✅ PRIORITÉ 2: DB
if coordsSource == "" {
if dLat, ok := getFloatFromMap(command, "dest_latitude"); ok && dLat != 0 {
destLat = dLat
}
if dLon, ok := getFloatFromMap(command, "dest_longitude"); ok && dLon != 0 {
destLon = dLon
}
if destLat != 0 && destLon != 0 {
coordsSource = "DB"
}
}
// ✅ PRIORITÉ 3: Géocodage
if coordsSource == "" {
geoService := c.MustGet("geoService").(*services.GeoService)
address, _ := command["adresse"].(string)
if address != "" && address != "Adresse non spécifiée" {
location, err := geoService.GeocodeAddress(address)
if err == nil {
destLat = location.Latitude
destLon = location.Longitude
coordsSource = "GEOCODING"
}
}
}
if destLat == 0 || destLon == 0 {
c.JSON(http.StatusOK, gin.H{
"can_validate": false,
"reason": "Coordonnées de destination non disponibles",
})
return
}
// Calculer la distance
distance := services.CalculateDistance(
services.Coordinates{Latitude: livreurLat, Longitude: livreurLon},
services.Coordinates{Latitude: destLat, Longitude: destLon},
)
distanceMeters := distance * 1000
canValidate := distance <= MAX_DELIVERY_VALIDATION_DISTANCE_KM
c.JSON(http.StatusOK, gin.H{
"can_validate": canValidate,
"your_position": gin.H{
"latitude": livreurLat,
"longitude": livreurLon,
},
"destination": gin.H{
"latitude": destLat,
"longitude": destLon,
"address": command["adresse"],
"source": coordsSource,
},
"distance_meters": int(distanceMeters),
"max_allowed_meters": MAX_DELIVERY_VALIDATION_DISTANCE_METERS,
"remaining_meters": maxInt(0, int(distanceMeters)-MAX_DELIVERY_VALIDATION_DISTANCE_METERS),
"message": func() string {
if canValidate {
return "Vous pouvez valider cette livraison"
}
return fmt.Sprintf("Rapprochez-vous de %.0f mètres pour valider", distanceMeters-float64(MAX_DELIVERY_VALIDATION_DISTANCE_METERS))
}(),
})
}
// ============================================
// 3️⃣ DÉMARRER UNE LIVRAISON (PASSER EN IN_ROUTE)
// ============================================
@@ -395,14 +150,3 @@ func StartDelivery(c *gin.Context) {
"status": "en_route",
})
}
// ============================================
// HELPERS
// ============================================
func maxInt(a, b int) int {
if a > b {
return a
}
return b
}
@@ -54,7 +54,7 @@ var (
// ============================================
// validateClientToken valide un token client
func validateClientToken(tokenString string, database *db.Database) (*ClientClaims, error) {
func validateClientToken(tokenString string) (*ClientClaims, error) {
tokenString = strings.TrimSpace(tokenString)
if tokenString == "" {
return nil, fmt.Errorf("token vide")
@@ -103,7 +103,7 @@ func validateClientToken(tokenString string, database *db.Database) (*ClientClai
}
// validateAdminToken valide un token admin
func validateAdminToken(tokenString string, database *db.Database) (*AdminClaims, error) {
func validateAdminToken(tokenString string) (*AdminClaims, error) {
tokenString = strings.TrimSpace(tokenString)
if tokenString == "" {
return nil, fmt.Errorf("token vide")
@@ -161,7 +161,7 @@ func ClientMiddleware(c *gin.Context) {
tokenStr := strings.TrimPrefix(authHeader, "Bearer ")
database := c.MustGet("database").(*db.Database)
claims, err := validateClientToken(tokenStr, database)
claims, err := validateClientToken(tokenStr)
if err != nil {
log.Printf("❌ [CLIENT-MWARE] Token invalide: %v", err)
c.JSON(http.StatusUnauthorized, gin.H{"error": "Token invalide"})
@@ -205,7 +205,7 @@ func AdminMiddleware(c *gin.Context) {
tokenStr := strings.TrimPrefix(authHeader, "Bearer ")
database := c.MustGet("database").(*db.Database)
claims, err := validateAdminToken(tokenStr, database)
claims, err := validateAdminToken(tokenStr)
if err != nil {
log.Printf("❌ [ADMIN-MWARE] Token invalide: %v", err)
c.JSON(http.StatusUnauthorized, gin.H{"error": "Token admin invalide"})
@@ -258,7 +258,7 @@ func CabineMiddleware(c *gin.Context) {
tokenStr := strings.TrimPrefix(authHeader, "Bearer ")
database := c.MustGet("database").(*db.Database)
claims, err := validateAdminToken(tokenStr, database)
claims, err := validateAdminToken(tokenStr)
if err != nil {
log.Printf("❌ [CABINE-MWARE] Token invalide: %v", err)
c.JSON(http.StatusUnauthorized, gin.H{"error": "Token invalide"})
@@ -312,7 +312,7 @@ func LivreurMiddleware(c *gin.Context) {
tokenStr := strings.TrimPrefix(authHeader, "Bearer ")
database := c.MustGet("database").(*db.Database)
claims, err := validateAdminToken(tokenStr, database)
claims, err := validateAdminToken(tokenStr)
if err != nil {
log.Printf("❌ [LIVREUR-MWARE] Token invalide: %v", err)
c.JSON(http.StatusUnauthorized, gin.H{"error": "Token invalide"})
@@ -507,94 +507,3 @@ func LoginRateLimitMiddleware(c *gin.Context) {
c.Header("X-RateLimit-Remaining", strconv.FormatInt(10-count, 10))
c.Next()
}
// ============================================
// HELPER MIDDLEWARE
// ============================================
// VerifyAuthHeader vérifie que le header Authorization est valide
func VerifyAuthHeader(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
log.Printf("❌ [AUTH-HEADER] Authorization header manquant")
c.JSON(http.StatusUnauthorized, gin.H{
"error": "Authorization header manquant",
"hint": "Utilisez: Authorization: Bearer <token>",
})
c.Abort()
return
}
// Vérifier le format "Bearer <token>"
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || parts[0] != "Bearer" {
log.Printf("❌ [AUTH-HEADER] Format invalide: %s", authHeader)
c.JSON(http.StatusUnauthorized, gin.H{
"error": "Format Authorization invalide",
"hint": "Utilisez: Authorization: Bearer <token>",
})
c.Abort()
return
}
log.Printf("✅ [AUTH-HEADER] Format valide")
c.Next()
}
// SessionErrorRecovery récupère les erreurs de session
func SessionErrorRecovery(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
log.Printf("❌ [SESSION-ERROR] Erreur système: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur serveur - Session compromise",
})
}
}()
c.Next()
if len(c.Errors) > 0 {
log.Printf("⚠️ [SESSION] Erreur handler: %v", c.Errors)
}
}
// LogSessionMiddleware log toutes les infos de session
func LogSessionMiddleware(c *gin.Context) {
username, _ := c.Get("username")
clientID, _ := c.Get("client_id")
sessionID, _ := c.Get("session_id")
log.Printf("📊 [SESSION-LOG] %s %s | user=%v | client_id=%v | session=%v",
c.Request.Method, c.Request.URL.Path, username, clientID, sessionID)
c.Next()
log.Printf("📊 [SESSION-LOG] Response: %d", c.Writer.Status())
}
// LoadClientContext charge les infos du client en contexte
func LoadClientContext(c *gin.Context, database *db.Database) (*db.SessionData, error) {
clientID, ok := c.Get("client_id")
if !ok {
return nil, fmt.Errorf("client_id manquant du contexte")
}
clientIDInt := clientID.(int)
// Récupérer la session
session, err := database.GetClientSession(clientIDInt)
if err != nil {
return nil, err
}
return session, nil
}
func DatabaseMiddleware(db *db.Database) gin.HandlerFunc {
return func(c *gin.Context) {
c.Set("database", db)
c.Next()
}
}
+21
View File
@@ -0,0 +1,21 @@
package models
import "time"
type WeekdayRow struct {
DOW int `gorm:"column:dow"`
Count int `gorm:"column:count"`
}
type DayRow struct {
Day time.Time `gorm:"column:day"`
Count int `gorm:"column:count"`
}
type ProductRow struct {
ProductID int `gorm:"column:product_id"`
Name string `gorm:"column:name"`
Quantity float64 `gorm:"column:total_quantity"`
OrderCount int `gorm:"column:order_count"`
Revenue float64 `gorm:"column:revenue"`
}
-1
View File
@@ -139,7 +139,6 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services
// ============================================
adminAuthGroupV2 := router.Group("/api/v2/admin/auth")
{
//adminAuthGroupV2.POST("/register", handlers.RegisterAdmin)
adminAuthGroupV2.POST("/login", middleware.LoginRateLimitMiddleware, handlers.LoginAdmin)
adminAuthGroupV2.POST("/logout", handlers.LogoutAdmin)
}