chore: add new features

This commit is contained in:
2026-03-05 19:09:16 +01:00
parent 532584edb2
commit 5ca480ea0b
23 changed files with 695 additions and 284 deletions
+91
View File
@@ -1000,3 +1000,94 @@ func (d *Database) ApproveDeliveryAtomic(commandID int, username string) (int, e
return totalPoints, nil
}
// ApproveDeliveryAtomicByStaff - Confirmation de réception par admin ou cabine à la place du client
func (d *Database) ApproveDeliveryAtomicByStaff(commandID int, staffUsername string) (int, string, error) {
log.Printf("🔒 [ApproveAtomicStaff] START - cmd=%d, staff=%s", commandID, staffUsername)
tx, err := d.Begin()
if err != nil {
return 0, "", fmt.Errorf("erreur transaction: %w", err)
}
defer tx.Rollback()
var currentStatus, clientUsername, livreurAssign string
err = tx.QueryRow(`
SELECT status, username, COALESCE(livreur_assign, '')
FROM commandes
WHERE id = $1
FOR UPDATE
`, commandID).Scan(&currentStatus, &clientUsername, &livreurAssign)
if err == sql.ErrNoRows {
return 0, "", fmt.Errorf("commande non trouvée")
}
if err != nil {
return 0, "", fmt.Errorf("erreur lecture commande: %w", err)
}
if currentStatus != "livre" {
return 0, "", fmt.Errorf("commande doit être en statut 'livre' (statut actuel: %s)", currentStatus)
}
result, err := tx.Exec(`
UPDATE commandes
SET status = 'approved', updated_at = CURRENT_TIMESTAMP
WHERE id = $1 AND status = 'livre'
`, commandID)
if err != nil {
return 0, "", fmt.Errorf("erreur mise à jour statut: %w", err)
}
rows, _ := result.RowsAffected()
if rows == 0 {
return 0, "", fmt.Errorf("commande déjà approuvée ou modifiée")
}
totalPoints, err := d.CalculateAndAddPointsForCommandTx(tx, commandID, clientUsername)
if err != nil {
return 0, "", fmt.Errorf("erreur attribution points: %w", err)
}
_, err = tx.Exec(`
UPDATE clients
SET command = command + 1, updated_at = CURRENT_TIMESTAMP
WHERE username = $1
`, clientUsername)
if err != nil {
log.Printf("⚠️ [ApproveAtomicStaff] Erreur incrémentation compteur: %v", err)
}
_, err = tx.Exec(`
INSERT INTO command_logs (command_id, status, message, author, created_at)
VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP)
`, commandID, "approved",
fmt.Sprintf("Réception confirmée par %s au nom du client %s - %d points attribués", staffUsername, clientUsername, totalPoints),
staffUsername)
if err != nil {
log.Printf("⚠️ [ApproveAtomicStaff] Erreur log: %v", err)
}
if err := tx.Commit(); err != nil {
return 0, "", fmt.Errorf("erreur commit: %w", err)
}
log.Printf("🎉 [ApproveAtomicStaff] SUCCÈS - cmd=%d approuvée par %s, %d points → client %s",
commandID, staffUsername, totalPoints, clientUsername)
if livreurAssign != "" {
go func() {
if err := d.CompleteDeliveryAndProcessNext(livreurAssign, commandID); err != nil {
log.Printf("⚠️ [ApproveAtomicStaff] Erreur queue: %v", err)
}
}()
}
go func() {
Redis.Del(RedisCtx, fmt.Sprintf("command:%d", commandID))
Redis.Del(RedisCtx, fmt.Sprintf("client:%s", clientUsername))
Redis.Del(RedisCtx, fmt.Sprintf("client:%s:commands", clientUsername))
}()
return totalPoints, clientUsername, nil
}
+1
View File
@@ -43,6 +43,7 @@ func (db *Database) CreateProduct(product interface{}) error {
log.Printf("📦 [DB CreateProduct] Category: %s", p.GetCategory())
log.Printf("📦 [DB CreateProduct] Description: %s", p.GetDescription())
log.Printf("📦 [DB CreateProduct] Stock: %.2f", p.GetStock())
log.Printf("📦 [DB CreateProduct] Unit: %s", p.GetUnit())
log.Printf("📦 [DB CreateProduct] Nombre de prix: %d", len(p.GetPrices()))
}
+9 -9
View File
@@ -80,7 +80,7 @@ type ProfileResponse struct {
var (
clientTokenDuration = 5 * time.Hour
adminTokenDuration = 2 * time.Hour
adminTokenDuration = 10 * time.Hour
userJWTSecret = []byte(os.Getenv("USER_JWT_SECRET")) // ✅ Pour clients
adminJWTSecret = []byte(os.Getenv("ADMIN_JWT_SECRET")) // ✅ Pour admin/cabine/livreur
)
@@ -287,7 +287,7 @@ func LoginClient(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
client, err := database.GetClientByUsername(req.Username)
if err != nil {
if err != nil || client == nil {
log.Printf("❌ [LOGIN_CLIENT] Client non trouvé: %s", req.Username)
c.JSON(http.StatusUnauthorized, gin.H{"error": "Identifiants invalides"})
return
@@ -327,13 +327,13 @@ func LoginClient(c *gin.Context) {
TokenType: "Bearer",
ExpiresIn: int(clientTokenDuration.Seconds()),
User: gin.H{
"id": client.ID,
"username": client.Username,
"nom": client.Nom,
"prenom": client.Prenom,
"telephone": client.Telephone,
"role": "client",
"session_id": sessionID,
"id": client.ID,
"username": client.Username,
"nom": client.Nom,
"prenom": client.Prenom,
"telephone": client.Telephone,
"role": "client",
"session_id": sessionID,
"must_change_password": client.MustChangePassword,
},
})
+79 -22
View File
@@ -305,6 +305,51 @@ func ApproveDelivery(c *gin.Context) {
})
}
// ============================================
// 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, clientUsername, err := database.ApproveDeliveryAtomicByStaff(commandID, staffUsername)
if err != nil {
log.Printf("❌ [STAFF_APPROVE] Erreur: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Impossible de confirmer la réception: " + err.Error()})
return
}
log.Printf("✅ [STAFF_APPROVE] %d points attribués au client %s", totalPoints, clientUsername)
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Réception confirmée",
"command_id": commandID,
"client_username": clientUsername,
"points_earned": totalPoints,
})
}
// ============================================
// APPROBATION PAR ADMIN
// ============================================
@@ -464,37 +509,49 @@ func GetAvailableDeliveryPersons(c *gin.Context) {
}
// AssignDeliveryPerson assigne manuellement un livreur à une commande
// POST /api/v1/admin/commands/:id/assign
// 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)
// ✅ SÉCURITÉ: Admin seulement
if c.GetString("role") != "admin" {
// ✅ 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
}
commandID, err := strconv.Atoi(c.Param("id"))
if err != nil {
// 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
}
var req struct {
LivreurUsername string `json:"livreur_username" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Données invalides",
"details": err.Error(),
})
return
// 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",
"details": err.Error(),
})
return
}
livreurUsername = req.LivreurUsername
}
log.Printf("👤 [ASSIGN] Assignation cmd %d à livreur %s", commandID, req.LivreurUsername)
log.Printf("👤 [ASSIGN] Assignation cmd %d à livreur %s", commandID, livreurUsername)
adminUsername, _ := c.Get("username")
if err := database.AssignDeliveryPerson(commandID, req.LivreurUsername); err != nil {
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",
@@ -504,17 +561,17 @@ func AssignDeliveryPerson(c *gin.Context) {
}
database.AddCommandLog(commandID, "support",
fmt.Sprintf("Livreur '%s' assigné manuellement par %s", req.LivreurUsername, adminUsername),
adminUsername.(string))
fmt.Sprintf("Livreur '%s' assigné manuellement par %s", livreurUsername, staffUsername),
staffUsername.(string))
log.Printf("✅ [ASSIGN] Commande %d assignée à %s", commandID, req.LivreurUsername)
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": req.LivreurUsername,
"assigned_by": adminUsername,
"livreur": livreurUsername,
"assigned_by": staffUsername,
})
}
+8
View File
@@ -12,6 +12,7 @@ import (
"log"
"net/http"
"os"
"time"
"github.com/gin-contrib/cors"
"github.com/gin-contrib/sessions"
@@ -21,6 +22,13 @@ import (
)
func main() {
// Forcer la timezone Europe/Paris (UTC+1/+2)
if loc, err := time.LoadLocation("Europe/Paris"); err == nil {
time.Local = loc
} else {
log.Printf("⚠️ Impossible de charger la timezone Europe/Paris: %v", err)
}
// Chargement des variables d'environnement
if err := godotenv.Load(); err != nil {
log.Println("⚠️ Aucun fichier .env trouvé, utilisation des valeurs par défaut.")
+3
View File
@@ -168,6 +168,7 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services
adminGroupV2.GET("/orders/:id", handlers.GetCommandByID)
adminGroupV2.PUT("/orders/:id/address", handlers.UpdateCommandAddress)
adminGroupV2.POST("/orders/:id/force-validate", handlers.ValidateDelivery)
adminGroupV2.POST("/orders/:id/confirm-reception", handlers.StaffApproveDelivery)
// ============================================
// ⭐ AUTO-ASSIGNATION GPS - ROUTES CRITIQUES
@@ -234,6 +235,8 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services
cabineGroupV1.Use(middleware.CabineMiddleware)
{
cabineGroupV1.GET("/commands/:id/items", handlers.ShowItems)
cabineGroupV1.POST("/commands/:id/confirm-reception", handlers.StaffApproveDelivery)
cabineGroupV1.POST("/commands/:id/assign", handlers.AssignDeliveryPerson)
cabineGroupV1.PUT("/items/:item_id/status", handlers.UpdateItemStatus)
cabineGroupV1.GET("/commands/:id/deliveryman/location", handlers.GetDeliverymanLocationForCommand)
cabineGroupV1.GET("/all/deliveryman", handlers.GetAllDeliveryMen)