chore: add new features
This commit is contained in:
@@ -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(¤tStatus, &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
|
||||
}
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user