chore: refacto
This commit is contained in:
@@ -1,14 +1,12 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"gestion/db"
|
||||
"gestion/models"
|
||||
"gestion/utils"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -19,109 +17,16 @@ import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// ============================================
|
||||
// TYPES JWT CLAIMS (Duplicés dans middleware aussi)
|
||||
// ============================================
|
||||
|
||||
type ClientClaims struct {
|
||||
ClientID int `json:"client_id"`
|
||||
Username string `json:"username"`
|
||||
Role string `json:"role"`
|
||||
SessionID string `json:"session_id"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
type AdminClaims struct {
|
||||
UserID int `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
Role string `json:"role"`
|
||||
SessionID string `json:"session_id"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// STRUCTURES REQUÊTE / RÉPONSE
|
||||
// ============================================
|
||||
|
||||
type LoginRequest struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
type RegisterClientRequest struct {
|
||||
Username string `json:"username" binding:"required,min=3,max=50"`
|
||||
Password string `json:"password" binding:"required,min=8"`
|
||||
Nom string `json:"nom" binding:"required,min=2,max=100"`
|
||||
Prenom string `json:"prenom" binding:"required,min=2,max=100"`
|
||||
Telephone string `json:"telephone" binding:"required"`
|
||||
}
|
||||
|
||||
type RegisterAdminRequest struct {
|
||||
Username string `json:"username" binding:"required,min=3,max=50"`
|
||||
Password string `json:"password" binding:"required,min=8"`
|
||||
Role string `json:"role" binding:"required,oneof=admin cabine livreur"`
|
||||
}
|
||||
|
||||
type LoginResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
User interface{} `json:"user"`
|
||||
}
|
||||
|
||||
type ProfileResponse struct {
|
||||
Username string `json:"username"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// VARIABLES & CONSTANTS
|
||||
// ============================================
|
||||
|
||||
var (
|
||||
clientTokenDuration = 5 * 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
|
||||
userJWTSecret = []byte(os.Getenv("USER_JWT_SECRET"))
|
||||
adminJWTSecret = []byte(os.Getenv("ADMIN_JWT_SECRET"))
|
||||
)
|
||||
|
||||
// ============================================
|
||||
// UTILITAIRES
|
||||
// ============================================
|
||||
|
||||
func generateSessionID() string {
|
||||
bytes := make([]byte, 16)
|
||||
rand.Read(bytes)
|
||||
return hex.EncodeToString(bytes)
|
||||
}
|
||||
|
||||
// validatePhoneNumber valide le format du numéro de téléphone
|
||||
func validatePhoneNumber(phone string) bool {
|
||||
// Supprimer espaces/tirets
|
||||
clean := regexp.MustCompile(`[\s\-\(\)]`).ReplaceAllString(phone, "")
|
||||
|
||||
// Format international ou national
|
||||
validFormat := regexp.MustCompile(`^(\+33|0)[1-9]\d{8}$`)
|
||||
return validFormat.MatchString(clean)
|
||||
}
|
||||
|
||||
func normalizePhoneNumber(phone string) string {
|
||||
clean := regexp.MustCompile(`[\s\-\(\)]`).ReplaceAllString(phone, "")
|
||||
|
||||
// Convertir 06... en +336...
|
||||
if strings.HasPrefix(clean, "0") {
|
||||
return "+33" + clean[1:]
|
||||
}
|
||||
return clean
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// GENERATION TOKENS
|
||||
// ============================================
|
||||
|
||||
func generateClientToken(client *models.Client) (string, error) {
|
||||
sessionID := generateSessionID()
|
||||
claims := ClientClaims{
|
||||
sessionID := utils.GenerateSessionID()
|
||||
claims := models.ClientClaims{
|
||||
ClientID: client.ID,
|
||||
Username: client.Username,
|
||||
Role: "client",
|
||||
@@ -135,7 +40,7 @@ func generateClientToken(client *models.Client) (string, error) {
|
||||
},
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
tokenString, err := token.SignedString(userJWTSecret) // ✅ UTILISER userJWTSecret (CLIENT)
|
||||
tokenString, err := token.SignedString(userJWTSecret)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -143,8 +48,8 @@ func generateClientToken(client *models.Client) (string, error) {
|
||||
}
|
||||
|
||||
func generateAdminToken(user *models.User) (string, error) {
|
||||
sessionID := generateSessionID()
|
||||
claims := AdminClaims{
|
||||
sessionID := utils.GenerateSessionID()
|
||||
claims := models.AdminClaims{
|
||||
UserID: user.ID,
|
||||
Username: user.Username,
|
||||
Role: user.Role, // ← "admin" ou "cabine" ou "livreur"
|
||||
@@ -158,21 +63,16 @@ func generateAdminToken(user *models.User) (string, error) {
|
||||
},
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
tokenString, err := token.SignedString(adminJWTSecret) // ✅ UTILISER adminJWTSecret (ADMIN/CABINE/LIVREUR)
|
||||
tokenString, err := token.SignedString(adminJWTSecret)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return tokenString, nil
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// HANDLERS AUTHENTIFICATION CLIENT
|
||||
// ============================================
|
||||
|
||||
// RegisterClient crée un nouveau compte client
|
||||
// POST /api/v1/auth/register
|
||||
func RegisterClient(c *gin.Context) {
|
||||
var req RegisterClientRequest
|
||||
var req models.RegisterClientRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
log.Printf("❌ [REGISTER_CLIENT] Erreur binding: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
@@ -183,7 +83,7 @@ func RegisterClient(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Validation téléphone
|
||||
if !validatePhoneNumber(req.Telephone) {
|
||||
if !utils.ValidatePhoneNumber(req.Telephone) {
|
||||
log.Printf("❌ [REGISTER_CLIENT] Téléphone invalide: %s", req.Telephone)
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Numéro de téléphone invalide",
|
||||
@@ -191,7 +91,7 @@ func RegisterClient(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
normalizedPhone := normalizePhoneNumber(req.Telephone)
|
||||
normalizedPhone := utils.NormalizePhoneNumber(req.Telephone)
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
|
||||
// Vérifier username unique
|
||||
@@ -256,9 +156,7 @@ func RegisterClient(c *gin.Context) {
|
||||
|
||||
client.Password = ""
|
||||
|
||||
log.Printf("✅ [REGISTER_CLIENT] Client créé: %s (ID=%d)", client.Username, client.ID)
|
||||
|
||||
c.JSON(http.StatusCreated, LoginResponse{
|
||||
c.JSON(http.StatusCreated, models.LoginResponse{
|
||||
AccessToken: token,
|
||||
TokenType: "Bearer",
|
||||
ExpiresIn: int(clientTokenDuration.Seconds()),
|
||||
@@ -275,20 +173,19 @@ func RegisterClient(c *gin.Context) {
|
||||
}
|
||||
|
||||
// AdminCreateClient crée un client depuis l'interface admin (sans session ni token)
|
||||
// POST /api/v2/admin/protected/clients
|
||||
func AdminCreateClient(c *gin.Context) {
|
||||
var req RegisterClientRequest
|
||||
var req models.RegisterClientRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"})
|
||||
return
|
||||
}
|
||||
|
||||
if !validatePhoneNumber(req.Telephone) {
|
||||
if !utils.ValidatePhoneNumber(req.Telephone) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Numéro de téléphone invalide"})
|
||||
return
|
||||
}
|
||||
|
||||
normalizedPhone := normalizePhoneNumber(req.Telephone)
|
||||
normalizedPhone := utils.NormalizePhoneNumber(req.Telephone)
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
|
||||
if existing, _ := database.GetClientByUsername(req.Username); existing != nil {
|
||||
@@ -322,8 +219,6 @@ func AdminCreateClient(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("✅ [ADMIN_CREATE_CLIENT] Client créé par admin: %s (ID=%d)", client.Username, client.ID)
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"message": "Client créé avec succès",
|
||||
"client": gin.H{
|
||||
@@ -337,9 +232,8 @@ func AdminCreateClient(c *gin.Context) {
|
||||
}
|
||||
|
||||
// LoginClient authentifie un client
|
||||
// POST /api/v1/auth/login
|
||||
func LoginClient(c *gin.Context) {
|
||||
var req LoginRequest
|
||||
var req models.LoginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
log.Printf("❌ [LOGIN_CLIENT] Erreur binding: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"})
|
||||
@@ -361,7 +255,6 @@ func LoginClient(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// ✅ LIGNE 302 CORRIGÉE - Gérer l'erreur !
|
||||
token, err := generateClientToken(client)
|
||||
if err != nil {
|
||||
log.Printf("❌ [LOGIN_CLIENT] Erreur génération token: %v", err)
|
||||
@@ -382,9 +275,7 @@ func LoginClient(c *gin.Context) {
|
||||
log.Printf("⚠️ [LOGIN_CLIENT] Erreur session Redis: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("✅ [LOGIN_CLIENT] Client authentifié: %s", req.Username)
|
||||
|
||||
c.JSON(http.StatusOK, LoginResponse{
|
||||
c.JSON(http.StatusOK, models.LoginResponse{
|
||||
AccessToken: token,
|
||||
TokenType: "Bearer",
|
||||
ExpiresIn: int(clientTokenDuration.Seconds()),
|
||||
@@ -402,7 +293,6 @@ func LoginClient(c *gin.Context) {
|
||||
}
|
||||
|
||||
// ChangePassword permet à un client de changer son mot de passe
|
||||
// PUT /api/v1/auth/change-password
|
||||
func ChangePassword(c *gin.Context) {
|
||||
var req struct {
|
||||
CurrentPassword string `json:"current_password" binding:"required"`
|
||||
@@ -442,16 +332,13 @@ func ChangePassword(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("✅ [CHANGE_PASSWORD] Mot de passe changé: ID=%d", clientID)
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "Mot de passe mis à jour avec succès"})
|
||||
}
|
||||
|
||||
// LogoutClient déconnecte un client
|
||||
// POST /api/v1/auth/logout
|
||||
func LogoutClient(c *gin.Context) {
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
|
||||
// Invalider la session Redis
|
||||
clientID, hasClientID := c.Get("client_id")
|
||||
if hasClientID && clientID != nil {
|
||||
if err := database.InvalidateSession(clientID.(int)); err != nil {
|
||||
@@ -459,26 +346,18 @@ func LogoutClient(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// Révoquer le JWT token
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader != "" && strings.HasPrefix(authHeader, "Bearer ") {
|
||||
tokenStr := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
database.RevokeToken(tokenStr)
|
||||
}
|
||||
|
||||
log.Printf("✅ [LOGOUT_CLIENT] Client déconnecté")
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Déconnexion réussie"})
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// HANDLERS AUTHENTIFICATION ADMIN
|
||||
// ============================================
|
||||
|
||||
// RegisterAdmin crée un nouvel utilisateur admin/cabine/livreur
|
||||
// POST /api/v1/auth/admin/register
|
||||
func RegisterAdmin(c *gin.Context) {
|
||||
var req RegisterAdminRequest
|
||||
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"})
|
||||
@@ -487,7 +366,6 @@ func RegisterAdmin(c *gin.Context) {
|
||||
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
|
||||
// Vérifier que l'user n'existe pas
|
||||
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é"})
|
||||
@@ -507,9 +385,6 @@ func RegisterAdmin(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("✅ [REGISTER_ADMIN] User créé: %s (role=%s, ID=%d)", user.Username, user.Role, user.ID)
|
||||
|
||||
// Générer le token
|
||||
token, err := generateAdminToken(user)
|
||||
if err != nil {
|
||||
log.Printf("❌ [REGISTER_ADMIN] Erreur génération token: %v", err)
|
||||
@@ -517,11 +392,6 @@ func RegisterAdmin(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("✅ [REGISTER_ADMIN] Token généré: %s...", token[:50])
|
||||
|
||||
// ============================================
|
||||
// ✅ CRITICAL FIX: ENREGISTRER LE TOKEN EN DB
|
||||
// ============================================
|
||||
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)
|
||||
@@ -529,11 +399,9 @@ func RegisterAdmin(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("✅ [REGISTER_ADMIN] Token enregistré en DB pour user ID: %d", user.ID)
|
||||
|
||||
user.Password = ""
|
||||
|
||||
c.JSON(http.StatusCreated, LoginResponse{
|
||||
c.JSON(http.StatusCreated, models.LoginResponse{
|
||||
AccessToken: token,
|
||||
TokenType: "Bearer",
|
||||
ExpiresIn: int(adminTokenDuration.Seconds()),
|
||||
@@ -542,10 +410,8 @@ func RegisterAdmin(c *gin.Context) {
|
||||
}
|
||||
|
||||
// LoginAdmin authentifie un admin/cabine/livreur
|
||||
// POST /api/v1/auth/admin/login
|
||||
// ✅ AMÉLIORÉ: Meilleure gestion d'erreurs
|
||||
func LoginAdmin(c *gin.Context) {
|
||||
var req LoginRequest
|
||||
var req models.LoginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
log.Printf("❌ [LOGIN_ADMIN] Erreur binding: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"})
|
||||
@@ -561,14 +427,12 @@ func LoginAdmin(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Vérifier que c'est un admin/cabine/livreur
|
||||
if user.Role != "admin" && user.Role != "cabine" && user.Role != "livreur" {
|
||||
log.Printf("❌ [LOGIN_ADMIN] Rôle invalide: %s", user.Role)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Accès non autorisé"})
|
||||
return
|
||||
}
|
||||
|
||||
// Vérifier le mot de passe
|
||||
if user.Password == "" {
|
||||
log.Printf("❌ [LOGIN_ADMIN] Mot de passe vide en base")
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Identifiants invalides"})
|
||||
@@ -581,7 +445,6 @@ func LoginAdmin(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Générer le token
|
||||
token, _ := generateAdminToken(user)
|
||||
|
||||
expiresAt := time.Now().Add(adminTokenDuration)
|
||||
@@ -591,9 +454,7 @@ func LoginAdmin(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("✅ [LOGIN_ADMIN] User authentifié: %s (role=%s)", user.Username, user.Role)
|
||||
|
||||
c.JSON(http.StatusOK, LoginResponse{
|
||||
c.JSON(http.StatusOK, models.LoginResponse{
|
||||
AccessToken: token,
|
||||
TokenType: "Bearer",
|
||||
ExpiresIn: int(adminTokenDuration.Seconds()),
|
||||
@@ -606,7 +467,6 @@ func LoginAdmin(c *gin.Context) {
|
||||
}
|
||||
|
||||
// LogoutAdmin déconnecte un admin/cabine/livreur
|
||||
// POST /api/v1/auth/admin/logout
|
||||
func LogoutAdmin(c *gin.Context) {
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
|
||||
@@ -645,14 +505,14 @@ func GetCurrentClient(c *gin.Context) {
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"client": gin.H{
|
||||
"id": client.ID,
|
||||
"username": client.Username,
|
||||
"nom": client.Nom,
|
||||
"prenom": client.Prenom,
|
||||
"telephone": client.Telephone,
|
||||
"command": client.Command,
|
||||
"point": client.Point,
|
||||
"amende": client.Amende,
|
||||
"id": client.ID,
|
||||
"username": client.Username,
|
||||
"nom": client.Nom,
|
||||
"prenom": client.Prenom,
|
||||
"telephone": client.Telephone,
|
||||
"command": client.Command,
|
||||
"amende": client.Amende,
|
||||
"points_extra": client.PointsExtra,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -675,7 +535,7 @@ func GetCurrentAdmin(c *gin.Context) {
|
||||
log.Printf("✅ [GET_CURRENT_ADMIN] User récupéré: %s", user.Username)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"user": ProfileResponse{
|
||||
"user": models.ProfileResponse{
|
||||
Username: user.Username,
|
||||
Role: user.Role,
|
||||
},
|
||||
@@ -795,8 +655,7 @@ func GetAllClients(c *gin.Context) {
|
||||
"prenom": cl.Prenom,
|
||||
"telephone": cl.Telephone,
|
||||
"command": cl.Command,
|
||||
"point": cl.Point,
|
||||
"points_zipette": cl.PointZipette,
|
||||
"points_extra": cl.PointsExtra,
|
||||
"amende": cl.Amende,
|
||||
"cancellations_count": cl.CancellationsCount,
|
||||
"last_penalty_reason": cl.LastPenaltyReason,
|
||||
|
||||
@@ -9,8 +9,10 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"gestion/db"
|
||||
"gestion/utils"
|
||||
"log"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
@@ -22,7 +24,6 @@ import (
|
||||
// ============================================
|
||||
|
||||
// SetCommandDestinationCoordinates stocke les coordonnées destination en Redis
|
||||
// POST /api/v2/admin/protected/orders/:id/set-destination
|
||||
func SetCommandDestinationCoordinates(c *gin.Context) {
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
|
||||
@@ -69,15 +70,10 @@ func SetCommandDestinationCoordinates(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Vérifier que la commande existe
|
||||
command, err := database.GetCommandByID(commandID)
|
||||
if err != nil {
|
||||
if !utils.CheckCommand(commandID, database) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Commande non trouvée"})
|
||||
return
|
||||
}
|
||||
log.Printf("Command: %v", command)
|
||||
|
||||
// Stocker en Redis avec format JSON
|
||||
destCacheKey := fmt.Sprintf("command:destination:%d", commandID)
|
||||
coordsJSON, _ := json.Marshal(map[string]float64{
|
||||
"lat": req.Latitude,
|
||||
@@ -134,12 +130,12 @@ func GetClientProfile(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"client": gin.H{
|
||||
"id": client.ID,
|
||||
"username": client.Username,
|
||||
"command": client.Command,
|
||||
"point": client.Point,
|
||||
"amende": client.Amende,
|
||||
"created_at": client.CreatedAt,
|
||||
"id": client.ID,
|
||||
"username": client.Username,
|
||||
"command": client.Command,
|
||||
"amende": client.Amende,
|
||||
"points_extra": client.PointsExtra,
|
||||
"created_at": client.CreatedAt,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -170,8 +166,8 @@ func GetClientFullHistory(c *gin.Context) {
|
||||
"client": gin.H{
|
||||
"username": client.Username,
|
||||
"total_commands": client.Command,
|
||||
"points": client.Point,
|
||||
"amende": client.Amende,
|
||||
"points_extra": client.PointsExtra,
|
||||
},
|
||||
"commands": commands,
|
||||
"count": len(commands),
|
||||
@@ -209,15 +205,7 @@ func UpdateCommandAddressCabine(c *gin.Context) {
|
||||
status, _ := command["status"].(string)
|
||||
|
||||
allowedStatuses := []string{"pending", "", "assigned"}
|
||||
isAllowed := false
|
||||
for _, s := range allowedStatuses {
|
||||
if status == s {
|
||||
isAllowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !isAllowed {
|
||||
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,
|
||||
@@ -288,10 +276,6 @@ func GetLivreurPosition(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 4. DELIVERY TRACKING CLIENT (SANS GPS)
|
||||
// ============================================
|
||||
|
||||
func GetDeliveryTrackingClient(c *gin.Context) {
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
|
||||
@@ -394,10 +378,6 @@ func GetDeliveryTracking(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 6. DELIVERY ISSUES
|
||||
// ============================================
|
||||
|
||||
func GetDeliveryIssues(c *gin.Context) {
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
|
||||
@@ -630,24 +610,6 @@ func ForceValidateDelivery(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
validStatuses := []string{"assigned", "in_transit", "en_route", "en_cours", "pending", "priority"}
|
||||
isValidStatus := false
|
||||
for _, vs := range validStatuses {
|
||||
if status == vs {
|
||||
isValidStatus = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !isValidStatus && status != "livre" {
|
||||
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
|
||||
}
|
||||
|
||||
if status == "livre" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Cette commande a déjà été validée",
|
||||
@@ -656,6 +618,16 @@ func ForceValidateDelivery(c *gin.Context) {
|
||||
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{
|
||||
@@ -668,7 +640,6 @@ func ForceValidateDelivery(c *gin.Context) {
|
||||
clientUsername, _ := command["username"].(string)
|
||||
livreurAssign, _ := command["livreur_assign"].(string)
|
||||
|
||||
// Notifier le client
|
||||
if clientUsername != "" {
|
||||
clientMsg := fmt.Sprintf("Votre commande #%d a été livrée. Merci !", commandID)
|
||||
database.NotifyClient(clientUsername, commandID, "livre", clientMsg)
|
||||
@@ -678,12 +649,11 @@ func ForceValidateDelivery(c *gin.Context) {
|
||||
log.Printf("⚠️ Erreur compteur commandes: %v", err)
|
||||
}
|
||||
|
||||
if err := database.AddClientPoints(clientUsername, 10); err != nil {
|
||||
if err := database.AddClientPointsByCategory(clientUsername, 10, ""); err != nil {
|
||||
log.Printf("⚠️ Erreur ajout points: %v", err)
|
||||
}
|
||||
|
||||
if livreurAssign != "" {
|
||||
log.Printf("📦 Commande %d validée de force par admin - Optimisation queue de %s...", commandID, livreurAssign)
|
||||
err := database.CompleteDeliveryAndProcessNext(livreurAssign, commandID)
|
||||
if err != nil {
|
||||
log.Printf("⚠️ Erreur optimisation: %v", err)
|
||||
|
||||
@@ -18,10 +18,6 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ============================================
|
||||
// RATE LIMITING
|
||||
// ============================================
|
||||
|
||||
var (
|
||||
cancelRateLimitMap = make(map[string][]time.Time)
|
||||
cancelMaxRequests = 5 // Max 5 annulations
|
||||
@@ -49,16 +45,10 @@ func checkCancelRateLimit(key string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// HELPERS DE SÉCURITÉ
|
||||
// ============================================
|
||||
|
||||
func validateReason(reason string) string {
|
||||
// Limiter la longueur
|
||||
if len(reason) > 500 {
|
||||
reason = reason[:500]
|
||||
}
|
||||
// Sanitizer
|
||||
reason = strings.Map(func(r rune) rune {
|
||||
if r < 32 || r == 127 {
|
||||
return -1
|
||||
@@ -73,10 +63,6 @@ func validateReason(reason string) string {
|
||||
return reason
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 1️⃣ ANNULATION PAR LE CLIENT - VERSION SÉCURISÉE
|
||||
// ============================================
|
||||
|
||||
func CancelCommandByClient(c *gin.Context) {
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
|
||||
@@ -115,19 +101,12 @@ func CancelCommandByClient(c *gin.Context) {
|
||||
|
||||
req.Reason = validateReason(req.Reason)
|
||||
|
||||
log.Printf("🚫 [CANCEL_CLIENT] Client %s annule cmd %d (force=%v)", username, commandID, req.Force)
|
||||
|
||||
// ============================================
|
||||
// UTILISER LA FONCTION ATOMIQUE
|
||||
// ============================================
|
||||
penalty, pointsLost, err := database.CancelCommandAtomic(commandID, username, req.Reason, req.Force)
|
||||
penalty, err := database.CancelCommandAtomic(commandID, username, req.Reason, req.Force)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("❌ [CANCEL_CLIENT] Erreur: %v", err)
|
||||
|
||||
// ✅ GESTION SPÉCIALE POUR "confirmation requise"
|
||||
if err.Error() == "confirmation requise" {
|
||||
// ✅ RÉCUPÉRER LES INFORMATIONS DE LA COMMANDE
|
||||
command, errCmd := database.GetCommandByID(commandID)
|
||||
if errCmd != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Commande non trouvée"})
|
||||
@@ -137,28 +116,14 @@ func CancelCommandByClient(c *gin.Context) {
|
||||
livreurAssign, _ := command["livreur_assign"].(string)
|
||||
currentStatus, _ := command["status"].(string)
|
||||
|
||||
// ✅ VÉRIFIER SI ETA EXISTE (VERSION CORRIGÉE)
|
||||
hasETA := false
|
||||
if livreurAssign != "" {
|
||||
// ✅ FIX: Utiliser la nouvelle fonction qui vérifie VRAIMENT l'ETA
|
||||
hasETA = database.CheckCommandETAExistsAndValid(commandID)
|
||||
}
|
||||
|
||||
// ✅ CALCULER LA PÉNALITÉ QUI SERA APPLIQUÉE
|
||||
nextPenalty, _ := database.CalculateCancellationPenalty(username)
|
||||
cancelCount, _ := database.GetClientCancellationsCount(username)
|
||||
|
||||
// ✅ RÉCUPÉRER LES POINTS ACTUELS
|
||||
client, _ := database.GetClientByUsername(username)
|
||||
currentPointsWeed := 0
|
||||
currentPointsZipette := 0
|
||||
if client != nil {
|
||||
currentPointsWeed = client.Point
|
||||
currentPointsZipette = client.PointZipette
|
||||
}
|
||||
totalPoints := currentPointsWeed + currentPointsZipette
|
||||
|
||||
// ✅ CONSTRUIRE LA RÉPONSE EN FONCTION DE hasETA
|
||||
response := gin.H{
|
||||
"success": false,
|
||||
"warning": true,
|
||||
@@ -170,7 +135,6 @@ func CancelCommandByClient(c *gin.Context) {
|
||||
}
|
||||
|
||||
if hasETA {
|
||||
// ⚠️ CAS 1: LIVREUR EN ROUTE (ETA définie) = PÉNALITÉ TOTALE
|
||||
log.Printf("⚠️ [CANCEL_CLIENT] Annulation tardive avec ETA - Status: %s, Livreur: %s", currentStatus, livreurAssign)
|
||||
|
||||
response["message"] = "⚠️ Un livreur est en route vers votre adresse (ETA définie)"
|
||||
@@ -180,27 +144,21 @@ func CancelCommandByClient(c *gin.Context) {
|
||||
"has_eta": true,
|
||||
}
|
||||
response["penalty_warning"] = gin.H{
|
||||
"will_apply": true,
|
||||
"penalty_amount": nextPenalty,
|
||||
"current_violations": cancelCount,
|
||||
"current_points_weed": currentPointsWeed,
|
||||
"current_points_zipette": currentPointsZipette,
|
||||
"total_points": totalPoints,
|
||||
"points_will_reset": true,
|
||||
"will_apply": true,
|
||||
"penalty_amount": nextPenalty,
|
||||
"current_violations": cancelCount,
|
||||
"message": fmt.Sprintf(
|
||||
"⚠️ ATTENTION: Une amende de %d points sera appliquée ET tous vos points (%d weed/hash + %d zipette = %d total) seront remis à zéro!",
|
||||
nextPenalty, currentPointsWeed, currentPointsZipette, totalPoints,
|
||||
"⚠️ ATTENTION: Une amende de %d sera appliquée pour annulation tardive",
|
||||
nextPenalty,
|
||||
),
|
||||
"scale": gin.H{
|
||||
"1st_cancel": "20 points + remise à zéro TOTALE",
|
||||
"2nd_cancel": "50 points + remise à zéro TOTALE",
|
||||
"3rd_cancel": "100 points + remise à zéro TOTALE",
|
||||
"4th+_cancel": "150 points + remise à zéro TOTALE",
|
||||
"your_next": fmt.Sprintf("%d points + remise à zéro de tous vos %d points", nextPenalty, totalPoints),
|
||||
"1st_cancel": 20,
|
||||
"2nd_cancel": 50,
|
||||
"3rd_cancel": 100,
|
||||
"4th+_cancel": 150,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
// ℹ️ CAS 2: LIVREUR ASSIGNÉ MAIS PAS EN ROUTE (PAS D'ETA) = PAS DE PÉNALITÉ
|
||||
log.Printf("ℹ️ [CANCEL_CLIENT] Livreur assigné mais pas d'ETA - Annulation sans pénalité")
|
||||
|
||||
response["message"] = "ℹ️ Un livreur est assigné mais n'est pas encore en route"
|
||||
@@ -274,11 +232,8 @@ func CancelCommandByClient(c *gin.Context) {
|
||||
|
||||
if penalty > 0 {
|
||||
response["penalty"] = gin.H{
|
||||
"penalty_points": penalty,
|
||||
"points_weed_lost": pointsLost["weed"],
|
||||
"points_zipette_lost": pointsLost["zipette"],
|
||||
"total_points_lost": pointsLost["weed"] + pointsLost["zipette"],
|
||||
"warning": "Une pénalité a été appliquée et vos points ont été remis à zéro",
|
||||
"penalty_amount": penalty,
|
||||
"warning": "Une amende a été appliquée pour annulation tardive",
|
||||
}
|
||||
} else {
|
||||
response["info"] = "Aucune pénalité appliquée"
|
||||
@@ -325,10 +280,6 @@ func GetMyCancellationHistory(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// LISTE DES COMMANDES ANNULÉES - VERSION SÉCURISÉE
|
||||
// ============================================
|
||||
|
||||
func GetAllCancelledOrders(c *gin.Context) {
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
|
||||
@@ -345,7 +296,6 @@ func GetAllCancelledOrders(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// ✅ VALIDATION des paramètres
|
||||
filterUsername := c.Query("username")
|
||||
if len(filterUsername) > 100 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Username trop long"})
|
||||
@@ -373,14 +323,14 @@ func GetAllCancelledOrders(c *gin.Context) {
|
||||
}
|
||||
|
||||
// ✅ ENRICHIR les données (sans exposer d'infos sensibles inutiles)
|
||||
var enrichedOrders []map[string]interface{}
|
||||
var enrichedOrders []map[string]any
|
||||
for _, order := range cancelledOrders {
|
||||
orderID, _ := order["id"].(int)
|
||||
|
||||
items, _ := database.GetCommandItems(orderID)
|
||||
logs, _ := database.GetCommandLogs(orderID)
|
||||
|
||||
var cancellationLog map[string]interface{}
|
||||
var cancellationLog map[string]any
|
||||
for _, logEntry := range logs {
|
||||
status, _ := logEntry["status"].(string)
|
||||
if status == "cancelled" {
|
||||
@@ -389,7 +339,7 @@ func GetAllCancelledOrders(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
enrichedOrder := map[string]interface{}{
|
||||
enrichedOrder := map[string]any{
|
||||
"id": order["id"],
|
||||
"username": order["username"],
|
||||
"total_prix": order["total_prix"],
|
||||
@@ -419,10 +369,6 @@ func GetAllCancelledOrders(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SUPPRESSION PAR CABINE - VERSION SÉCURISÉE
|
||||
// ============================================
|
||||
|
||||
func DeleteCommandByCabine(c *gin.Context) {
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
|
||||
@@ -447,12 +393,10 @@ func DeleteCommandByCabine(c *gin.Context) {
|
||||
|
||||
log.Printf("🗑️ [DELETE_COMMAND] %s (%s) supprime cmd %d", username, userRole, commandID)
|
||||
|
||||
// ✅ UTILISER LA FONCTION ATOMIQUE
|
||||
err = database.DeleteCommandAtomic(commandID, username, userRole)
|
||||
if err != nil {
|
||||
log.Printf("❌ [DELETE_COMMAND] Erreur: %v", err)
|
||||
|
||||
// ❌ Ne pas exposer les détails de l'erreur
|
||||
if err.Error() == "commande non trouvée" {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Commande non trouvée"})
|
||||
} else {
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// GET /api/v1/categories — public
|
||||
func GetCategories(c *gin.Context) {
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
|
||||
@@ -27,7 +26,6 @@ func GetCategories(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// POST /api/v2/admin/protected/categories — admin
|
||||
func CreateCategory(c *gin.Context) {
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
|
||||
@@ -111,7 +109,6 @@ func UpdateCategory(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// DELETE /api/v2/admin/protected/categories/:id — admin
|
||||
func DeleteCategory(c *gin.Context) {
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
|
||||
@@ -131,6 +128,5 @@ func DeleteCategory(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("✅ [CATEGORIES] Supprimée: %d", id)
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "Catégorie supprimée"})
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
)
|
||||
|
||||
// GetCommandStatus - Statut temps réel d'une commande
|
||||
// GET /api/v1/commands/:id/status
|
||||
func GetCommandStatus(c *gin.Context) {
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
|
||||
@@ -28,16 +27,12 @@ func GetCommandStatus(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("📊 [STATUS] Client %s demande statut cmd %d", usernameStr, commandID)
|
||||
|
||||
// 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 PROPRIÉTÉ
|
||||
cmdUsername, _ := command["username"].(string)
|
||||
if cmdUsername != usernameStr {
|
||||
log.Printf("❌ [STATUS] Accès refusé - cmd de %s demandée par %s", cmdUsername, usernameStr)
|
||||
@@ -47,10 +42,8 @@ func GetCommandStatus(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Récupérer ETA
|
||||
etaData, _ := database.GetCommandETA(commandID)
|
||||
|
||||
// Récupérer infos livreur (si assigné)
|
||||
livreurInfo := gin.H{
|
||||
"assigned": false,
|
||||
}
|
||||
@@ -63,7 +56,6 @@ func GetCommandStatus(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// Mapper le statut en message lisible
|
||||
statusMessage := getStatusMessage(command["status"].(string))
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
@@ -80,7 +72,6 @@ func GetCommandStatus(c *gin.Context) {
|
||||
}
|
||||
|
||||
// GetMyCommandsWithTracking - Liste des commandes avec suivi
|
||||
// GET /api/v1/my-commands
|
||||
func GetMyCommandsWithTracking(c *gin.Context) {
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
|
||||
@@ -147,7 +138,6 @@ func GetMyCommandsWithTracking(c *gin.Context) {
|
||||
}
|
||||
|
||||
// GetCommandTracking - Suivi détaillé d'une commande
|
||||
// GET /api/v1/commands/:id/tracking
|
||||
func GetCommandTracking(c *gin.Context) {
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
|
||||
@@ -174,13 +164,10 @@ func GetCommandTracking(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Récupérer logs
|
||||
logs, _ := database.GetCommandLogs(commandID)
|
||||
|
||||
// ETA
|
||||
etaData, _ := database.GetCommandETA(commandID)
|
||||
|
||||
// Timeline (basé sur les logs)
|
||||
timeline := buildTimeline(logs)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
@@ -194,11 +181,6 @@ func GetCommandTracking(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// HELPERS
|
||||
// ============================================
|
||||
|
||||
// getStatusMessage retourne un message lisible pour le client
|
||||
func getStatusMessage(status string) string {
|
||||
messages := map[string]string{
|
||||
"pending": "⏳ En attente d'assignation",
|
||||
@@ -219,7 +201,7 @@ func getStatusMessage(status string) string {
|
||||
}
|
||||
|
||||
// buildTimeline construit une timeline depuis les logs
|
||||
func buildTimeline(logs []map[string]interface{}) []gin.H {
|
||||
func buildTimeline(logs []map[string]any) []gin.H {
|
||||
timeline := make([]gin.H, 0)
|
||||
|
||||
for _, logEntry := range logs {
|
||||
|
||||
@@ -3,6 +3,7 @@ package handlers
|
||||
import (
|
||||
"fmt"
|
||||
"gestion/db"
|
||||
"gestion/utils"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@@ -42,9 +43,6 @@ func checkRateLimit(key string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// GESTION ADRESSE & ADMIN
|
||||
// ============================================
|
||||
func safeGetUsername(c *gin.Context) (string, error) {
|
||||
username, exists := c.Get("username")
|
||||
if !exists {
|
||||
@@ -71,13 +69,11 @@ func validateAddress(address string) error {
|
||||
}
|
||||
|
||||
// UpdateCommandAddress met à jour l'adresse de livraison d'une commande
|
||||
// PUT /api/v1/admin/commands/:id/address
|
||||
func UpdateCommandAddress(c *gin.Context) {
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
|
||||
// ✅ Vérification du rôle
|
||||
if c.GetString("role") != "admin" {
|
||||
log.Printf("❌ [UPD_ADDR] Accès refusé - role=%s", c.GetString("role"))
|
||||
userRole := c.GetString("role")
|
||||
if !utils.CheckRoleAdmin(c, userRole) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"})
|
||||
return
|
||||
}
|
||||
@@ -111,13 +107,11 @@ func UpdateCommandAddress(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// ✅ Validation de l'adresse
|
||||
if err := validateAddress(req.DeliveryAddress); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// ✅ Logs sanitizés
|
||||
log.Printf("📝 [UPD_ADDR] Admin %s modifie cmd %d", adminUsername, commandID)
|
||||
|
||||
command, err := database.GetCommandByID(commandID)
|
||||
@@ -156,13 +150,11 @@ func UpdateCommandAddress(c *gin.Context) {
|
||||
}
|
||||
|
||||
// ProposeAddressChange propose une nouvelle adresse au client pour validation
|
||||
// POST /api/v2/admin/protected/orders/:id/propose-address
|
||||
// POST /api/v1/cabine/commands/:id/propose-address
|
||||
func ProposeAddressChange(c *gin.Context) {
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
|
||||
userRole := c.GetString("role")
|
||||
if userRole != "admin" && userRole != "cabine" {
|
||||
if !utils.CheckRoleAdmin(c, userRole) && !utils.CheckRoleCabine(c, userRole) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"})
|
||||
return
|
||||
}
|
||||
@@ -218,7 +210,6 @@ func ProposeAddressChange(c *gin.Context) {
|
||||
database.NotifyClient(clientUsername, commandID, "address_proposal", msg)
|
||||
}
|
||||
|
||||
log.Printf("✅ [PROPOSE_ADDR] Commande %d - nouvelle adresse proposée par %s", commandID, staffUsername)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "Nouvelle adresse proposée au client",
|
||||
@@ -231,6 +222,11 @@ func ProposeAddressChange(c *gin.Context) {
|
||||
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"})
|
||||
@@ -276,8 +272,7 @@ func GetAllCommands(c *gin.Context) {
|
||||
username := c.Query("username")
|
||||
|
||||
userRole := c.GetString("role")
|
||||
if userRole != "admin" && userRole != "cabine" {
|
||||
log.Printf("❌ [VALIDATE] Accès refusé - role=%s", userRole)
|
||||
if !utils.CheckRoleAdmin(c, userRole) && !utils.CheckRoleCabine(c, userRole) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"})
|
||||
return
|
||||
}
|
||||
@@ -725,8 +720,7 @@ func GetClientCommandsHistory(c *gin.Context) {
|
||||
} else if client == nil {
|
||||
log.Printf("⚠️ [HISTORY] client est NIL!")
|
||||
} else {
|
||||
log.Printf("✅ [HISTORY] Client récupéré: username=%s, point=%d, point_zipette=%d",
|
||||
client.Username, client.Point, client.PointZipette)
|
||||
log.Printf("✅ [HISTORY] Client récupéré: username=%s", client.Username)
|
||||
}
|
||||
|
||||
resp := gin.H{
|
||||
@@ -760,7 +754,6 @@ func GetClientCommandsHistory(c *gin.Context) {
|
||||
"prenom": client.Prenom,
|
||||
"telephone": client.Telephone,
|
||||
"total_commands": client.Command,
|
||||
"points": client.Point,
|
||||
"pool_points": poolPoints,
|
||||
"pool_names": poolNames,
|
||||
"penalties": client.Amende,
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
)
|
||||
|
||||
// IPNWebhook - POST /api/v1/webhooks/nowpayments
|
||||
// Reçoit les callbacks de NowPayments lors des changements de statut
|
||||
func IPNWebhook(c *gin.Context) {
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
np, ok := c.MustGet("nowpayments").(*services.NowPaymentsClient)
|
||||
@@ -44,7 +43,6 @@ func IPNWebhook(c *gin.Context) {
|
||||
payment, err := database.GetCryptoPaymentByNowPaymentID(payload.PaymentID.String())
|
||||
if err != nil || payment == nil {
|
||||
log.Printf("[IPN] paiement introuvable: %s", payload.PaymentID.String())
|
||||
// 200 pour éviter les retries NowPayments
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,25 +1,18 @@
|
||||
// ============================================
|
||||
// handlers/delivery_handlers.go
|
||||
// 🔧 VERSION MODIFIÉE avec ETA automatique
|
||||
// ============================================
|
||||
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"gestion/db"
|
||||
"gestion/utils"
|
||||
"log"
|
||||
"math"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ============================================
|
||||
// 🔧 GetMyDeliveries
|
||||
// ============================================
|
||||
func GetMyDeliveries(c *gin.Context) {
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
|
||||
@@ -46,7 +39,6 @@ func GetMyDeliveries(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// ✨ FILTRAGE (SANS TÉLÉPHONE)
|
||||
filteredCommands := make([]gin.H, len(commands))
|
||||
for i, cmd := range commands {
|
||||
commandID, _ := cmd["id"].(int)
|
||||
@@ -172,10 +164,6 @@ func GetDeliveryDetails(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 🔧 UpdateDeliveryStatus - VERSION MODIFIÉE
|
||||
// ✅ CALCUL AUTOMATIQUE ETA lors du passage en "en_route"
|
||||
// ============================================
|
||||
func UpdateDeliveryStatus(c *gin.Context) {
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
|
||||
@@ -215,7 +203,6 @@ func UpdateDeliveryStatus(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// ✅ VÉRIFIER PROPRIÉTÉ
|
||||
livreurAssign, _ := command["livreur_assign"].(string)
|
||||
if livreurAssign != usernameStr {
|
||||
log.Printf("❌ Accès refusé - assigné à %s", livreurAssign)
|
||||
@@ -225,7 +212,6 @@ func UpdateDeliveryStatus(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// ✅ STATUTS VALIDES POUR LIVREUR (correspondant à la DB)
|
||||
validStatuses := []string{
|
||||
"assigned",
|
||||
"en_route",
|
||||
@@ -234,15 +220,7 @@ func UpdateDeliveryStatus(c *gin.Context) {
|
||||
"cancelled",
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -261,7 +239,7 @@ func UpdateDeliveryStatus(c *gin.Context) {
|
||||
destLon, _ := command["dest_longitude"].(float64)
|
||||
|
||||
if destLat != 0 && destLon != 0 {
|
||||
distance := calculateDistance(req.Latitude, req.Longitude, destLat, destLon)
|
||||
distance := utils.CalculateDistance(req.Latitude, req.Longitude, destLat, destLon)
|
||||
log.Printf("📍 [GPS] Distance: %.2f m", distance)
|
||||
|
||||
if distance > 100 {
|
||||
@@ -295,10 +273,8 @@ func UpdateDeliveryStatus(c *gin.Context) {
|
||||
if req.Status == "en_route" {
|
||||
log.Printf("🚗 [STATUS_LIVREUR] Passage en 'en_route' - Calcul ETA...")
|
||||
|
||||
// Récupérer les coordonnées destination
|
||||
var destLat, destLon float64
|
||||
|
||||
// 1. Essayer le cache Redis
|
||||
destCacheKey := fmt.Sprintf("command:destination:%d", commandID)
|
||||
destData, err := db.Redis.Get(db.RedisCtx, destCacheKey).Result()
|
||||
if err == nil && destData != "" {
|
||||
@@ -326,22 +302,29 @@ func UpdateDeliveryStatus(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Calculer l'ETA depuis la position du livreur
|
||||
if destLat != 0 && destLon != 0 {
|
||||
etaMinutes = database.CalculateETAForDeliveryman(usernameStr, destLat, destLon)
|
||||
|
||||
// Définir l'ETA dans Redis
|
||||
if err := database.SetCommandETA(commandID, etaMinutes); err != nil {
|
||||
log.Printf("⚠️ [STATUS_LIVREUR] Erreur définition ETA: %v", err)
|
||||
} else {
|
||||
log.Printf("✅ [STATUS_LIVREUR] ETA défini: %d minutes", etaMinutes)
|
||||
etaMessage = fmt.Sprintf("Arrivée prévue dans %d minutes", etaMinutes)
|
||||
if etaMinutes >= 60 {
|
||||
h := etaMinutes / 60
|
||||
m := etaMinutes % 60
|
||||
if m > 0 {
|
||||
etaMessage = fmt.Sprintf("Arrivée prévue dans %dh%02d", h, m)
|
||||
} else {
|
||||
etaMessage = fmt.Sprintf("Arrivée prévue dans %dh", h)
|
||||
}
|
||||
} else {
|
||||
etaMessage = fmt.Sprintf("Arrivée prévue dans %d minutes", etaMinutes)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Printf("⚠️ [STATUS_LIVREUR] Coordonnées destination manquantes - ETA par défaut")
|
||||
etaMinutes = 30 // Fallback
|
||||
etaMinutes = 30
|
||||
database.SetCommandETA(commandID, etaMinutes)
|
||||
etaMessage = "Arrivée prévue dans 30 minutes (estimation par défaut)"
|
||||
}
|
||||
|
||||
// Mettre à jour le statut du livreur en "delivering"
|
||||
@@ -352,7 +335,7 @@ func UpdateDeliveryStatus(c *gin.Context) {
|
||||
// Log
|
||||
message := req.Notes
|
||||
if message == "" {
|
||||
message = getDeliveryStatusMessage(req.Status)
|
||||
message = utils.GetDeliveryStatusMessage(req.Status)
|
||||
}
|
||||
if etaMessage != "" {
|
||||
message += fmt.Sprintf(" - %s", etaMessage)
|
||||
@@ -366,9 +349,21 @@ func UpdateDeliveryStatus(c *gin.Context) {
|
||||
switch req.Status {
|
||||
case "en_route":
|
||||
if etaMinutes > 0 {
|
||||
clientMsg = fmt.Sprintf("Votre commande #%d est en route ! Arrivée dans ~%d min", commandID, etaMinutes)
|
||||
var etaStr string
|
||||
if etaMinutes >= 60 {
|
||||
h := etaMinutes / 60
|
||||
m := etaMinutes % 60
|
||||
if m > 0 {
|
||||
etaStr = fmt.Sprintf("%dh%02d", h, m)
|
||||
} else {
|
||||
etaStr = fmt.Sprintf("%dh", h)
|
||||
}
|
||||
} else {
|
||||
etaStr = fmt.Sprintf("%d min", etaMinutes)
|
||||
}
|
||||
clientMsg = fmt.Sprintf("🛵 Votre commande #%d est en route ! Arrivée dans ~%s", commandID, etaStr)
|
||||
} else {
|
||||
clientMsg = fmt.Sprintf("Votre commande #%d est en route !", commandID)
|
||||
clientMsg = fmt.Sprintf("🛵 Votre commande #%d est en route !", commandID)
|
||||
}
|
||||
case "arrived":
|
||||
clientMsg = fmt.Sprintf("🛵 Votre livreur est là ! Il sera chez vous dans 5 minutes (commande #%d)", commandID)
|
||||
@@ -412,44 +407,3 @@ func UpdateDeliveryStatus(c *gin.Context) {
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// HELPERS
|
||||
// ============================================
|
||||
func calculateDistance(lat1, lon1, lat2, lon2 float64) float64 {
|
||||
const earthRadiusKm = 6371
|
||||
const metersPerKm = 1000
|
||||
|
||||
lat1Rad := degreesToRadians(lat1)
|
||||
lon1Rad := degreesToRadians(lon1)
|
||||
lat2Rad := degreesToRadians(lat2)
|
||||
lon2Rad := degreesToRadians(lon2)
|
||||
|
||||
dLat := lat2Rad - lat1Rad
|
||||
dLon := lon2Rad - lon1Rad
|
||||
|
||||
a := math.Sin(dLat/2)*math.Sin(dLat/2) +
|
||||
math.Cos(lat1Rad)*math.Cos(lat2Rad)*
|
||||
math.Sin(dLon/2)*math.Sin(dLon/2)
|
||||
|
||||
c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
|
||||
return earthRadiusKm * c * metersPerKm
|
||||
}
|
||||
|
||||
func degreesToRadians(degrees float64) float64 {
|
||||
return degrees * math.Pi / 180
|
||||
}
|
||||
|
||||
func getDeliveryStatusMessage(status string) string {
|
||||
messages := map[string]string{
|
||||
"assigned": "Commande assignée",
|
||||
"en_route": "En route vers le client",
|
||||
"arrived": "Arrivé à destination",
|
||||
"livre": "Livraison effectuée",
|
||||
"cancelled": "Livraison annulée",
|
||||
}
|
||||
if msg, ok := messages[status]; ok {
|
||||
return msg
|
||||
}
|
||||
return fmt.Sprintf("Statut changé: %s", status)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ package handlers
|
||||
import (
|
||||
"fmt"
|
||||
"gestion/db"
|
||||
"gestion/utils"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@@ -16,12 +17,7 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ============================================
|
||||
// 📊 GET DELIVERY PERSON DETAILS
|
||||
// ============================================
|
||||
|
||||
// GetDeliveryPersonDetails récupère les détails complets d'un livreur
|
||||
// GET /api/v2/admin/protected/delivery-persons/:username
|
||||
func GetDeliveryPersonDetails(c *gin.Context) {
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
|
||||
@@ -39,11 +35,6 @@ func GetDeliveryPersonDetails(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("👤 [GET_DELIVERY_DETAILS] Récupération détails pour: %s", username)
|
||||
|
||||
// ============================================
|
||||
// ÉTAPE 1: Récupérer les infos de base du livreur
|
||||
// ============================================
|
||||
livreur, err := database.GetUserByUsername(username)
|
||||
if err != nil {
|
||||
log.Printf("❌ [GET_DELIVERY_DETAILS] Livreur non trouvé: %v", err)
|
||||
@@ -64,9 +55,6 @@ func GetDeliveryPersonDetails(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// ÉTAPE 2: Récupérer le statut et la position GPS
|
||||
// ============================================
|
||||
status, err := database.GetDeliveryPersonStatus(username)
|
||||
if err != nil {
|
||||
log.Printf("⚠️ [GET_DELIVERY_DETAILS] Impossible de récupérer le statut: %v", err)
|
||||
@@ -83,22 +71,13 @@ func GetDeliveryPersonDetails(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// ÉTAPE 3: Récupérer les stats de livraison
|
||||
// ============================================
|
||||
queueSize, _ := database.GetDeliverymanQueueSize(username)
|
||||
currentCommand, _ := database.GetCurrentCommand(username)
|
||||
|
||||
// Compter les livraisons
|
||||
totalDeliveries, _ := database.CountDeliveriesByStatus(username, "")
|
||||
completedDeliveries, _ := database.CountDeliveriesByStatus(username, "approved")
|
||||
pendingDeliveries, _ := database.CountDeliveriesByStatus(username, "assigned,en_route,livre")
|
||||
|
||||
log.Printf("✅ [GET_DELIVERY_DETAILS] Détails récupérés pour %s", username)
|
||||
|
||||
// ============================================
|
||||
// RÉPONSE
|
||||
// ============================================
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"deliveryman": gin.H{
|
||||
@@ -116,19 +95,12 @@ func GetDeliveryPersonDetails(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 🔄 UPDATE DELIVERY PERSON STATUS
|
||||
// ============================================
|
||||
|
||||
// UpdateDeliveryPersonStatusAdmin modifie le statut d'un livreur (Admin)
|
||||
// PUT /api/v2/admin/protected/delivery-persons/:username/status
|
||||
func UpdateDeliveryPersonStatusAdmin(c *gin.Context) {
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
|
||||
// ✅ SÉCURITÉ: Admin seulement
|
||||
userRole := c.GetString("role")
|
||||
if userRole != "admin" {
|
||||
log.Printf("❌ [UPDATE_DELIVERY_STATUS] Accès refusé - role=%s", userRole)
|
||||
if !utils.CheckRoleAdmin(c, userRole) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"})
|
||||
return
|
||||
}
|
||||
@@ -172,9 +144,6 @@ func UpdateDeliveryPersonStatusAdmin(c *gin.Context) {
|
||||
|
||||
log.Printf("📝 [UPDATE_DELIVERY_STATUS] Modification: %s → %s", username, req.Status)
|
||||
|
||||
// ============================================
|
||||
// Vérifier que le livreur existe
|
||||
// ============================================
|
||||
livreur, err := database.GetUserByUsername(username)
|
||||
if err != nil {
|
||||
log.Printf("❌ [UPDATE_DELIVERY_STATUS] Livreur non trouvé")
|
||||
@@ -189,9 +158,6 @@ func UpdateDeliveryPersonStatusAdmin(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Mettre à jour le statut
|
||||
// ============================================
|
||||
err = database.UpdateDeliveryPersonStatus(username, req.Status)
|
||||
if err != nil {
|
||||
log.Printf("❌ [UPDATE_DELIVERY_STATUS] Erreur mise à jour: %v", err)
|
||||
@@ -215,16 +181,10 @@ func UpdateDeliveryPersonStatusAdmin(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 📊 GET DELIVERY PERSON STATS
|
||||
// ============================================
|
||||
|
||||
// GetDeliveryPersonStats récupère les statistiques d'un livreur
|
||||
// GET /api/v2/admin/protected/delivery-persons/:username/stats
|
||||
func GetDeliveryPersonStats(c *gin.Context) {
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
|
||||
// ✅ SÉCURITÉ: Admin seulement
|
||||
userRole := c.GetString("role")
|
||||
if userRole != "admin" {
|
||||
log.Printf("❌ [GET_DELIVERY_STATS] Accès refusé - role=%s", userRole)
|
||||
@@ -240,9 +200,6 @@ func GetDeliveryPersonStats(c *gin.Context) {
|
||||
|
||||
log.Printf("📊 [GET_DELIVERY_STATS] Calcul stats pour: %s", username)
|
||||
|
||||
// ============================================
|
||||
// Vérifier que le livreur existe
|
||||
// ============================================
|
||||
livreur, err := database.GetUserByUsername(username)
|
||||
if err != nil {
|
||||
log.Printf("❌ [GET_DELIVERY_STATS] Livreur non trouvé")
|
||||
@@ -306,12 +263,7 @@ func GetDeliveryPersonStats(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 📜 GET DELIVERY PERSON HISTORY
|
||||
// ============================================
|
||||
|
||||
// GetDeliveryPersonHistory récupère l'historique des livraisons d'un livreur
|
||||
// GET /api/v2/admin/protected/delivery-persons/:username/history?limit=20&offset=0
|
||||
func GetDeliveryPersonHistory(c *gin.Context) {
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
|
||||
@@ -394,10 +346,6 @@ func GetDeliveryPersonHistory(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 📍 UPDATE DELIVERY PERSON LOCATION
|
||||
// ============================================
|
||||
|
||||
// UpdateDeliveryPersonLocationAdmin modifie la position GPS d'un livreur (Admin)
|
||||
// PUT /api/v2/admin/protected/delivery-persons/:username/location
|
||||
func UpdateDeliveryPersonLocationAdmin(c *gin.Context) {
|
||||
@@ -579,7 +527,6 @@ func RemoveCommandFromQueue(c *gin.Context) {
|
||||
if err != nil {
|
||||
log.Printf("⚠️ [REMOVE_FROM_QUEUE] Impossible de réinitialiser le statut: %v", err)
|
||||
} else {
|
||||
// Retirer l'assignation du livreur
|
||||
database.UpdateCommandLivreur(commandID, "")
|
||||
log.Printf("✅ [REMOVE_FROM_QUEUE] Commande réinitialisée en 'pending'")
|
||||
}
|
||||
|
||||
@@ -18,11 +18,6 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ============================================
|
||||
// GET /api/v1/orders/:id/eta
|
||||
// ✅ CORRECTION: ETA visible UNIQUEMENT si status >= en_route
|
||||
// ============================================
|
||||
|
||||
func GetOrderETA(c *gin.Context) {
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
geoService := c.MustGet("geoService").(*services.GeoService)
|
||||
@@ -49,9 +44,6 @@ func GetOrderETA(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("📊 [ETA] START - commandID=%d, username=%s", commandID, username.(string))
|
||||
|
||||
// 3️⃣ RÉCUPÉRER LA COMMANDE
|
||||
command, err := database.GetCommandByID(commandID)
|
||||
if err != nil {
|
||||
log.Printf("❌ [ETA] Commande %d non trouvée", commandID)
|
||||
|
||||
@@ -22,8 +22,6 @@ import (
|
||||
// ============================================
|
||||
|
||||
// GeocodeAddress convertit une adresse en coordonnées GPS
|
||||
// POST /api/v1/geocode
|
||||
// Body: {"address": "1600 Amphitheatre Parkway, Mountain View, CA"}
|
||||
func GeocodeAddress(c *gin.Context) {
|
||||
geoService := c.MustGet("geoService").(*services.GeoService)
|
||||
|
||||
@@ -59,10 +57,6 @@ func GeocodeAddress(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// RECHERCHE DU LIVREUR LE PLUS PROCHE
|
||||
// ============================================
|
||||
|
||||
// FindNearestDeliveryPerson trouve le livreur le plus proche d'une adresse
|
||||
func FindNearestDeliveryPerson(c *gin.Context) {
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
@@ -181,8 +175,6 @@ func FindNearestDeliveryPerson(c *gin.Context) {
|
||||
// ============================================
|
||||
|
||||
// GetAllDeliveryDistances retourne tous les livreurs triés par distance
|
||||
// POST /api/v2/admin/protected/delivery/distances
|
||||
// Body: {"address": "123 Main St"} ou {"latitude": 48.8566, "longitude": 2.3522}
|
||||
func GetAllDeliveryDistances(c *gin.Context) {
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
geoService := c.MustGet("geoService").(*services.GeoService)
|
||||
|
||||
@@ -6,9 +6,11 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gestion/db"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -127,3 +129,49 @@ func GetCommandNavigationLinks(c *gin.Context) {
|
||||
"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) {
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
username := c.GetString("username")
|
||||
|
||||
commandID, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "ID 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 != username {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Cette commande ne vous est pas assignée"})
|
||||
return
|
||||
}
|
||||
|
||||
// Priorité : coordonnées GPS de la destination
|
||||
var wazeLink string
|
||||
destLat, hasLat := command["dest_latitude"].(float64)
|
||||
destLon, hasLon := command["dest_longitude"].(float64)
|
||||
if hasLat && hasLon && destLat != 0 && destLon != 0 {
|
||||
wazeLink = fmt.Sprintf("waze://?ll=%.6f,%.6f&navigate=yes", destLat, destLon)
|
||||
log.Printf("🗺️ [NAV_LINK] Lien coords pour cmd %d: %s", commandID, wazeLink)
|
||||
} else if adresse, _ := command["adresse"].(string); adresse != "" {
|
||||
wazeLink = fmt.Sprintf("waze://?q=%s&navigate=yes", url.QueryEscape(adresse))
|
||||
log.Printf("🗺️ [NAV_LINK] Lien adresse pour cmd %d: %s", commandID, wazeLink)
|
||||
} else {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Aucune destination disponible pour cette commande"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"command_id": commandID,
|
||||
"waze_app": wazeLink,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ func GetMyCompletedOrders(c *gin.Context) {
|
||||
response["client_stats"] = gin.H{
|
||||
"username": client.Username,
|
||||
"total_commands": client.Command,
|
||||
"points": client.Point,
|
||||
"points_extra": client.PointsExtra,
|
||||
"pool_points": poolPoints,
|
||||
"pool_names": poolNames,
|
||||
"penalties": client.Amende,
|
||||
@@ -169,7 +169,7 @@ func GetMyCompletedOrdersWithItems(c *gin.Context) {
|
||||
"prenom": client.Prenom,
|
||||
"telephone": client.Telephone,
|
||||
"total_commands": client.Command,
|
||||
"points": client.Point,
|
||||
"points_extra": client.PointsExtra,
|
||||
"penalties": client.Amende,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,54 +9,6 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// RegisterPushToken enregistre le push token Expo d'un client
|
||||
// POST /api/v1/push-token
|
||||
func RegisterPushToken(c *gin.Context) {
|
||||
clientID := c.GetInt("client_id")
|
||||
if clientID == 0 {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
PushToken string `json:"push_token" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "push_token requis"})
|
||||
return
|
||||
}
|
||||
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
if err := database.SaveClientPushToken(clientID, req.PushToken); err != nil {
|
||||
log.Printf("❌ [PUSH_TOKEN] Erreur sauvegarde token pour client %d: %v", clientID, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur enregistrement push token"})
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("✅ [PUSH_TOKEN] Token enregistré pour client %d", clientID)
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// UnregisterPushToken supprime le push token d'un client (au logout)
|
||||
// DELETE /api/v1/push-token
|
||||
func UnregisterPushToken(c *gin.Context) {
|
||||
clientID := c.GetInt("client_id")
|
||||
if clientID == 0 {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
|
||||
return
|
||||
}
|
||||
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
if err := database.DeleteClientPushToken(clientID); err != nil {
|
||||
log.Printf("❌ [PUSH_TOKEN] Erreur suppression token pour client %d: %v", clientID, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur suppression push token"})
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("✅ [PUSH_TOKEN] Token supprimé pour client %d", clientID)
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// GetClientNotifications retourne les notifications du client connecté
|
||||
// GET /api/v1/notifications
|
||||
func GetClientNotifications(c *gin.Context) {
|
||||
@@ -107,54 +59,6 @@ func GetClientNotifications(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// RegisterLivreurPushToken enregistre le push token Expo d'un livreur
|
||||
// POST /api/v1/livreur/push-token
|
||||
func RegisterLivreurPushToken(c *gin.Context) {
|
||||
username := c.GetString("username")
|
||||
if username == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
PushToken string `json:"push_token" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "push_token requis"})
|
||||
return
|
||||
}
|
||||
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
if err := database.SaveUserPushToken(username, req.PushToken); err != nil {
|
||||
log.Printf("❌ [LIVREUR_PUSH_TOKEN] Erreur sauvegarde token pour %s: %v", username, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur enregistrement push token"})
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("✅ [LIVREUR_PUSH_TOKEN] Token enregistré pour livreur %s", username)
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// UnregisterLivreurPushToken supprime le push token d'un livreur (au logout)
|
||||
// DELETE /api/v1/livreur/push-token
|
||||
func UnregisterLivreurPushToken(c *gin.Context) {
|
||||
username := c.GetString("username")
|
||||
if username == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
|
||||
return
|
||||
}
|
||||
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
if err := database.DeleteUserPushToken(username); err != nil {
|
||||
log.Printf("❌ [LIVREUR_PUSH_TOKEN] Erreur suppression token pour %s: %v", username, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur suppression push token"})
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("✅ [LIVREUR_PUSH_TOKEN] Token supprimé pour livreur %s", username)
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// GetLivreurNotifications retourne les notifications du livreur connecté
|
||||
// GET /api/v1/livreur/notifications
|
||||
func GetLivreurNotifications(c *gin.Context) {
|
||||
@@ -245,92 +149,6 @@ func MarkLivreurNotificationsRead(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// RegisterAdminPushToken enregistre le push token d'un admin
|
||||
// POST /api/v2/admin/protected/push-token
|
||||
func RegisterAdminPushToken(c *gin.Context) {
|
||||
username := c.GetString("username")
|
||||
if username == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
PushToken string `json:"push_token" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "push_token requis"})
|
||||
return
|
||||
}
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
if err := database.SaveUserPushToken(username, req.PushToken); err != nil {
|
||||
log.Printf("❌ [ADMIN_PUSH_TOKEN] Erreur sauvegarde pour %s: %v", username, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur enregistrement push token"})
|
||||
return
|
||||
}
|
||||
log.Printf("✅ [ADMIN_PUSH_TOKEN] Token enregistré pour admin %s", username)
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// UnregisterAdminPushToken supprime le push token d'un admin (au logout)
|
||||
// DELETE /api/v2/admin/protected/push-token
|
||||
func UnregisterAdminPushToken(c *gin.Context) {
|
||||
username := c.GetString("username")
|
||||
if username == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
|
||||
return
|
||||
}
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
if err := database.DeleteUserPushToken(username); err != nil {
|
||||
log.Printf("❌ [ADMIN_PUSH_TOKEN] Erreur suppression pour %s: %v", username, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur suppression push token"})
|
||||
return
|
||||
}
|
||||
log.Printf("✅ [ADMIN_PUSH_TOKEN] Token supprimé pour admin %s", username)
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// RegisterCabinePushToken enregistre le push token d'un agent cabine
|
||||
// POST /api/v1/cabine/push-token
|
||||
func RegisterCabinePushToken(c *gin.Context) {
|
||||
username := c.GetString("username")
|
||||
if username == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
PushToken string `json:"push_token" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "push_token requis"})
|
||||
return
|
||||
}
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
if err := database.SaveUserPushToken(username, req.PushToken); err != nil {
|
||||
log.Printf("❌ [CABINE_PUSH_TOKEN] Erreur sauvegarde pour %s: %v", username, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur enregistrement push token"})
|
||||
return
|
||||
}
|
||||
log.Printf("✅ [CABINE_PUSH_TOKEN] Token enregistré pour cabine %s", username)
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// UnregisterCabinePushToken supprime le push token d'un agent cabine (au logout)
|
||||
// DELETE /api/v1/cabine/push-token
|
||||
func UnregisterCabinePushToken(c *gin.Context) {
|
||||
username := c.GetString("username")
|
||||
if username == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
|
||||
return
|
||||
}
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
if err := database.DeleteUserPushToken(username); err != nil {
|
||||
log.Printf("❌ [CABINE_PUSH_TOKEN] Erreur suppression pour %s: %v", username, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur suppression push token"})
|
||||
return
|
||||
}
|
||||
log.Printf("✅ [CABINE_PUSH_TOKEN] Token supprimé pour cabine %s", username)
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// MarkNotificationsRead marque toutes les notifications comme lues
|
||||
// POST /api/v1/notifications/read
|
||||
func MarkNotificationsRead(c *gin.Context) {
|
||||
|
||||
@@ -295,8 +295,8 @@ func ValidateBasket(c *gin.Context) {
|
||||
var req struct {
|
||||
DeliveryAddress string `json:"delivery_address" binding:"required"`
|
||||
UseReferralBalance bool `json:"use_referral_balance"`
|
||||
PaymentMethod string `json:"payment_method"` // "cash" (défaut) ou "crypto"
|
||||
PayCurrency string `json:"pay_currency"` // ex: "btc", "eth", "ltc" (requis si crypto)
|
||||
PaymentMethod string `json:"payment_method"` // "cash" (défaut) ou "crypto"
|
||||
PayCurrency string `json:"pay_currency"` // ex: "btc", "eth", "ltc" (requis si crypto)
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil || req.DeliveryAddress == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Adresse de livraison requise"})
|
||||
@@ -310,6 +310,12 @@ func ValidateBasket(c *gin.Context) {
|
||||
}
|
||||
req.DeliveryAddress = cmd.DeliveryAddress
|
||||
|
||||
// Vérifier que le client a lié son compte Telegram
|
||||
if _, linked, err := database.GetClientTelegramChatID(usernameStr); err != nil || !linked {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Vous devez lier votre compte Telegram avant de commander"})
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("🛒 [CHECKOUT] Début checkout pour: %s", usernameStr)
|
||||
|
||||
// ============================================
|
||||
@@ -430,7 +436,7 @@ func ValidateBasket(c *gin.Context) {
|
||||
command, err := database.CreateCommandWithAddress(usernameStr, req.DeliveryAddress)
|
||||
if err != nil {
|
||||
if referralUsed > 0 {
|
||||
_ = database.RestoreReferralBalance(usernameStr, referralUsed)
|
||||
_ = database.CreditClientReferral(usernameStr, referralUsed)
|
||||
}
|
||||
log.Printf("❌ [CHECKOUT] Erreur création commande: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur création commande"})
|
||||
@@ -464,7 +470,7 @@ func ValidateBasket(c *gin.Context) {
|
||||
// Annuler la commande et restaurer le panier / parrainage
|
||||
_ = database.CancelCryptoCommand(commandID)
|
||||
if referralUsed > 0 {
|
||||
_ = database.RestoreReferralBalance(usernameStr, referralUsed)
|
||||
_ = database.CreditClientReferral(usernameStr, referralUsed)
|
||||
}
|
||||
log.Printf("❌ [CHECKOUT] Erreur création paiement NowPayments: %v", err)
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": "Impossible d'initier le paiement crypto"})
|
||||
@@ -520,14 +526,9 @@ func ValidateBasket(c *gin.Context) {
|
||||
if err == nil {
|
||||
log.Printf("📍 [CHECKOUT] Adresse géocodée: %.6f,%.6f", location.Latitude, location.Longitude)
|
||||
|
||||
activeLivreurs, err := database.GetAllActiveDeliveryPersons()
|
||||
if err == nil && len(activeLivreurs) > 0 {
|
||||
log.Printf("🚚 [CHECKOUT] %d livreurs actifs disponibles", len(activeLivreurs))
|
||||
|
||||
usernames := make([]string, len(activeLivreurs))
|
||||
for i, livreur := range activeLivreurs {
|
||||
usernames[i] = livreur.Username
|
||||
}
|
||||
usernames, errEligible := database.GetEligibleDeliverymenForCommand(commandID)
|
||||
if errEligible == nil && len(usernames) > 0 {
|
||||
log.Printf("🚚 [CHECKOUT] %d livreur(s) éligible(s) disponibles", len(usernames))
|
||||
|
||||
nearest, err := geoService.FindNearestDeliveryPersonFast(services.Coordinates{
|
||||
Latitude: location.Latitude,
|
||||
@@ -599,7 +600,7 @@ func ValidateBasket(c *gin.Context) {
|
||||
log.Printf("⚠️ [CHECKOUT] Aucun livreur trouvé: %v", err)
|
||||
}
|
||||
} else {
|
||||
log.Printf("⚠️ [CHECKOUT] Aucun livreur actif disponible")
|
||||
log.Printf("⚠️ [CHECKOUT] Aucun livreur éligible disponible")
|
||||
}
|
||||
} else {
|
||||
log.Printf("⚠️ [CHECKOUT] Erreur géocodage: %v", err)
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -654,41 +653,12 @@ func UpdateProduct(c *gin.Context) {
|
||||
|
||||
log.Printf("🔄 [UpdateProduct] %s met à jour produit #%d", username, id)
|
||||
|
||||
// ✅ UPDATE PRODUIT
|
||||
updateQuery := `
|
||||
UPDATE products
|
||||
SET name = $1, category = $2, description = $3, stock = $4, unit = $5, updated_at = $6
|
||||
WHERE id = $7
|
||||
`
|
||||
|
||||
_, err = database.Exec(updateQuery,
|
||||
updateData.Name,
|
||||
updateData.Category,
|
||||
updateData.Description,
|
||||
updateData.Stock,
|
||||
updateData.Unit,
|
||||
time.Now(),
|
||||
id,
|
||||
)
|
||||
if err != nil {
|
||||
if err := database.UpdateProduct(id, updateData.Name, updateData.Category, updateData.Description, updateData.Unit, updateData.Stock, updateData.Prices); err != nil {
|
||||
log.Printf("❌ [UpdateProduct] Erreur UPDATE: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur mise à jour"})
|
||||
return
|
||||
}
|
||||
|
||||
// ✅ UPDATE PRIX
|
||||
database.Exec(`DELETE FROM product_prices WHERE product_id = $1`, id)
|
||||
|
||||
for _, price := range updateData.Prices {
|
||||
_, err := database.Exec(`
|
||||
INSERT INTO product_prices (product_id, quantity, price)
|
||||
VALUES ($1, $2, $3)
|
||||
`, id, price.Quantity, price.Price)
|
||||
if err != nil {
|
||||
log.Printf("❌ [UpdateProduct] Erreur prix: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ RÉCUPÉRER LE PRODUIT MIS À JOUR
|
||||
updatedProduct, _ := database.GetProductByID(id)
|
||||
media, _ := database.GetMediaByProductID(id)
|
||||
@@ -702,10 +672,6 @@ func UpdateProduct(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// DELETE MEDIA - VERSION SÉCURISÉE
|
||||
// ============================================
|
||||
|
||||
func DeleteMedia(c *gin.Context) {
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
|
||||
@@ -756,8 +722,6 @@ func DeleteMedia(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// handlers/product_handlers_SECURED.go
|
||||
|
||||
func UploadMedia(c *gin.Context) {
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
|
||||
|
||||
@@ -962,7 +962,7 @@ func ResetClientPointAdmin(c *gin.Context) {
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
|
||||
extraPoolKey := ""
|
||||
if req.Pool >= 2 {
|
||||
if req.Pool >= 0 {
|
||||
if settings, err := database.GetSettings(); err == nil && req.Pool < len(settings.PointsPools) {
|
||||
extraPoolKey = settings.PointsPools[req.Pool].Key
|
||||
}
|
||||
|
||||
@@ -2,8 +2,11 @@ package handlers
|
||||
|
||||
import (
|
||||
"gestion/db"
|
||||
"gestion/models"
|
||||
"gestion/services"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -27,18 +30,18 @@ func GetPublicSettings(c *gin.Context) {
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"penalties_enabled": settings.PenaltiesEnabled,
|
||||
"show_amende_score": settings.ShowAmendeScore,
|
||||
"points_enabled": settings.PointsEnabled,
|
||||
"points_separated": len(settings.PointsPools) > 1,
|
||||
"pool_names": poolNames,
|
||||
"pool_keys": poolKeys,
|
||||
"referral_enabled": settings.ReferralEnabled,
|
||||
"delivery_schedule": settings.DeliverySchedule,
|
||||
"crypto_payment_enabled": settings.CryptoPaymentEnabled,
|
||||
"crypto_only": settings.CryptoOnly,
|
||||
"nowpayments_currencies": settings.NowPaymentsCurrencies,
|
||||
"success": true,
|
||||
"penalties_enabled": settings.PenaltiesEnabled,
|
||||
"show_amende_score": settings.ShowAmendeScore,
|
||||
"points_enabled": settings.PointsEnabled,
|
||||
"points_separated": len(settings.PointsPools) > 1,
|
||||
"pool_names": poolNames,
|
||||
"pool_keys": poolKeys,
|
||||
"referral_enabled": settings.ReferralEnabled,
|
||||
"delivery_schedule": settings.DeliverySchedule,
|
||||
"crypto_payment_enabled": settings.CryptoPaymentEnabled,
|
||||
"crypto_only": settings.CryptoOnly,
|
||||
"nowpayments_currencies": settings.NowPaymentsCurrencies,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -60,7 +63,7 @@ func GetSettings(c *gin.Context) {
|
||||
func UpdateSettings(c *gin.Context) {
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
|
||||
var req db.AppSettings
|
||||
var req models.AppSettings
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Paramètres invalides"})
|
||||
return
|
||||
@@ -72,7 +75,20 @@ func UpdateSettings(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("✅ [SETTINGS] Mise à jour: penalties=%v, pools=%d", req.PenaltiesEnabled, len(req.PointsPools))
|
||||
// Recharger le service Telegram si le token/username a changé
|
||||
if services.TelegramBot != nil {
|
||||
services.TelegramBot.Reload(req.TelegramBotToken, req.TelegramBotUsername)
|
||||
if req.TelegramBotToken != "" {
|
||||
log.Printf("✅ [SETTINGS] Service Telegram rechargé (username: %s)", req.TelegramBotUsername)
|
||||
if webhookURL := os.Getenv("TELEGRAM_WEBHOOK_URL"); webhookURL != "" {
|
||||
if err := services.TelegramBot.SetWebhook(webhookURL); err != nil {
|
||||
log.Printf("⚠️ [SETTINGS] Erreur enregistrement webhook Telegram: %v", err)
|
||||
} else {
|
||||
log.Printf("✅ [SETTINGS] Webhook Telegram enregistré: %s", webhookURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "settings": req})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"gestion/db"
|
||||
"gestion/models"
|
||||
"gestion/services"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func TelegramWebhook(c *gin.Context) {
|
||||
// Vérification du secret webhook
|
||||
secret := c.GetHeader("X-Telegram-Bot-Api-Secret-Token")
|
||||
if services.TelegramBot == nil || !services.TelegramBot.ValidateWebhookSecret(secret) {
|
||||
c.Status(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
var update models.TgUpdate
|
||||
if err := c.ShouldBindJSON(&update); err != nil {
|
||||
c.Status(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if update.Message == nil {
|
||||
c.Status(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
text := strings.TrimSpace(update.Message.Text)
|
||||
chatID := update.Message.Chat.ID
|
||||
|
||||
// Commande /start <token> — liaison de compte
|
||||
if token, ok := strings.CutPrefix(text, "/start "); ok {
|
||||
token = strings.TrimSpace(token)
|
||||
handleLinkAccount(c, token, chatID)
|
||||
return
|
||||
}
|
||||
|
||||
// Commande /start sans token — message d'accueil
|
||||
if text == "/start" {
|
||||
if services.TelegramBot != nil {
|
||||
services.TelegramBot.SendMessage(chatID,
|
||||
"👋 <b>Bienvenue !</b>\n\nPour lier votre compte, générez un token depuis l'application et envoyez <code>/start <token></code>.")
|
||||
}
|
||||
}
|
||||
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
func handleLinkAccount(c *gin.Context, token string, chatID int64) {
|
||||
if token == "" {
|
||||
if services.TelegramBot != nil {
|
||||
services.TelegramBot.SendMessage(chatID, "❌ Token manquant. Générez un nouveau token depuis l'application.")
|
||||
}
|
||||
c.Status(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
|
||||
username, role, err := db.ValidateAndConsumeLinkToken(token)
|
||||
if err != nil {
|
||||
log.Printf("⚠️ [TELEGRAM_LINK] Token invalide: %v", err)
|
||||
if services.TelegramBot != nil {
|
||||
services.TelegramBot.SendMessage(chatID, "❌ Token invalide ou expiré. Générez un nouveau token depuis l'application.")
|
||||
}
|
||||
c.Status(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
// Enregistrer le chat_id selon le rôle
|
||||
var saveErr error
|
||||
switch role {
|
||||
case "client":
|
||||
saveErr = database.SaveClientTelegramChatID(username, chatID)
|
||||
default:
|
||||
saveErr = database.SaveUserTelegramChatID(username, chatID)
|
||||
}
|
||||
|
||||
if saveErr != nil {
|
||||
log.Printf("❌ [TELEGRAM_LINK] Erreur sauvegarde chat_id pour %s (%s): %v", username, role, saveErr)
|
||||
if services.TelegramBot != nil {
|
||||
services.TelegramBot.SendMessage(chatID, "❌ Une erreur est survenue. Réessayez.")
|
||||
}
|
||||
c.Status(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("✅ [TELEGRAM_LINK] Compte %s (%s) lié au chat_id %d", username, role, chatID)
|
||||
if services.TelegramBot != nil {
|
||||
services.TelegramBot.SendMessage(chatID,
|
||||
"✅ <b>Compte lié avec succès !</b>\n\nVous recevrez désormais vos notifications ici.")
|
||||
}
|
||||
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
func GenerateClientLinkToken(c *gin.Context) {
|
||||
username := c.GetString("username")
|
||||
if username == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
|
||||
return
|
||||
}
|
||||
|
||||
if services.TelegramBot == nil || !services.TelegramBot.IsConfigured() {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Service Telegram non disponible"})
|
||||
return
|
||||
}
|
||||
|
||||
token, err := db.GenerateLinkToken(username, "client")
|
||||
if err != nil {
|
||||
log.Printf("❌ [TELEGRAM] Erreur génération token pour %s: %v", username, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur génération token"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"token": token,
|
||||
"link_url": "https://t.me/" + services.TelegramBot.BotUsername + "?start=" + token,
|
||||
"message": "/start " + token,
|
||||
"expires_in": 600,
|
||||
})
|
||||
}
|
||||
|
||||
func GenerateLivreurLinkToken(c *gin.Context) {
|
||||
username := c.GetString("username")
|
||||
if username == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
|
||||
return
|
||||
}
|
||||
|
||||
if services.TelegramBot == nil || !services.TelegramBot.IsConfigured() {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Service Telegram non disponible"})
|
||||
return
|
||||
}
|
||||
|
||||
token, err := db.GenerateLinkToken(username, "livreur")
|
||||
if err != nil {
|
||||
log.Printf("❌ [TELEGRAM] Erreur génération token pour livreur %s: %v", username, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur génération token"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"token": token,
|
||||
"link_url": "https://t.me/" + services.TelegramBot.BotUsername + "?start=" + token,
|
||||
"message": "/start " + token,
|
||||
"expires_in": 600,
|
||||
})
|
||||
}
|
||||
|
||||
func GenerateAdminLinkToken(c *gin.Context) {
|
||||
username := c.GetString("username")
|
||||
if username == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
|
||||
return
|
||||
}
|
||||
|
||||
if services.TelegramBot == nil || !services.TelegramBot.IsConfigured() {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Service Telegram non disponible"})
|
||||
return
|
||||
}
|
||||
|
||||
// Récupérer le rôle réel depuis la DB
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
user, err := database.GetUserByUsername(username)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Utilisateur introuvable"})
|
||||
return
|
||||
}
|
||||
|
||||
token, err := db.GenerateLinkToken(username, user.Role)
|
||||
if err != nil {
|
||||
log.Printf("❌ [TELEGRAM] Erreur génération token pour admin %s: %v", username, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur génération token"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"token": token,
|
||||
"link_url": "https://t.me/" + services.TelegramBot.BotUsername + "?start=" + token,
|
||||
"message": "/start " + token,
|
||||
"expires_in": 600,
|
||||
})
|
||||
}
|
||||
|
||||
// GET /api/v1/telegram/status
|
||||
func GetClientTelegramStatus(c *gin.Context) {
|
||||
username := c.GetString("username")
|
||||
if username == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
|
||||
return
|
||||
}
|
||||
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
_, linked, err := database.GetClientTelegramChatID(username)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur vérification"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"linked": linked,
|
||||
"enabled": services.TelegramBot != nil && services.TelegramBot.IsConfigured(),
|
||||
})
|
||||
}
|
||||
|
||||
// GET /api/v1/livreur/telegram/status
|
||||
func GetLivreurTelegramStatus(c *gin.Context) {
|
||||
username := c.GetString("username")
|
||||
if username == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
|
||||
return
|
||||
}
|
||||
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
_, linked, err := database.GetUserTelegramChatID(username)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur vérification"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"linked": linked,
|
||||
"enabled": services.TelegramBot != nil && services.TelegramBot.IsConfigured(),
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// DÉLIAISON TELEGRAM
|
||||
// ============================================
|
||||
|
||||
// DELETE /api/v1/telegram/unlink
|
||||
func UnlinkClientTelegram(c *gin.Context) {
|
||||
username := c.GetString("username")
|
||||
if username == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
|
||||
return
|
||||
}
|
||||
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
if err := database.DeleteClientTelegramChatID(username); err != nil {
|
||||
log.Printf("❌ [TELEGRAM_UNLINK] Erreur pour %s: %v", username, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur déliaison"})
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("✅ [TELEGRAM_UNLINK] Compte client %s délié", username)
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// DELETE /api/v1/livreur/telegram/unlink
|
||||
func UnlinkLivreurTelegram(c *gin.Context) {
|
||||
username := c.GetString("username")
|
||||
if username == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
|
||||
return
|
||||
}
|
||||
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
if err := database.DeleteUserTelegramChatID(username); err != nil {
|
||||
log.Printf("❌ [TELEGRAM_UNLINK] Erreur pour %s: %v", username, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur déliaison"})
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("✅ [TELEGRAM_UNLINK] Compte livreur %s délié", username)
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// DELETE /api/v2/admin/protected/telegram/unlink
|
||||
func UnlinkAdminTelegram(c *gin.Context) {
|
||||
username := c.GetString("username")
|
||||
if username == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
|
||||
return
|
||||
}
|
||||
|
||||
database := c.MustGet("database").(*db.Database)
|
||||
if err := database.DeleteUserTelegramChatID(username); err != nil {
|
||||
log.Printf("❌ [TELEGRAM_UNLINK] Erreur pour %s: %v", username, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur déliaison"})
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("✅ [TELEGRAM_UNLINK] Compte admin %s délié", username)
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
|
||||
"gestion/db"
|
||||
"gestion/models"
|
||||
"gestion/utils"
|
||||
)
|
||||
|
||||
// ============================================
|
||||
@@ -99,12 +100,12 @@ func UpdateMyProfile(c *gin.Context) {
|
||||
|
||||
// Mise à jour du téléphone
|
||||
if req.Telephone != "" && req.Telephone != client.Telephone {
|
||||
if !validatePhoneNumber(req.Telephone) {
|
||||
if !utils.ValidatePhoneNumber(req.Telephone) {
|
||||
log.Printf("❌ [UPDATE_MY_PROFILE] Téléphone invalide: %s", req.Telephone)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Numéro de téléphone invalide"})
|
||||
return
|
||||
}
|
||||
normalizedPhone := normalizePhoneNumber(req.Telephone)
|
||||
normalizedPhone := utils.NormalizePhoneNumber(req.Telephone)
|
||||
|
||||
// Vérifier que le téléphone n'est pas déjà utilisé
|
||||
if existingClient, _ := database.GetClientByTelephone(normalizedPhone); existingClient != nil && existingClient.ID != clientID {
|
||||
@@ -202,8 +203,8 @@ func UpdateClientByAdmin(c *gin.Context) {
|
||||
}
|
||||
|
||||
// ✅ LOG DEBUG - État initial
|
||||
log.Printf("📊 [UPDATE_CLIENT_ADMIN] État initial - Command: %d, Point: %d, PointZipette: %d, Amende: %.2f",
|
||||
client.Command, client.Point, client.PointZipette, client.Amende)
|
||||
log.Printf("📊 [UPDATE_CLIENT_ADMIN] État initial - Command: %d, Amende: %.2f",
|
||||
client.Command, client.Amende)
|
||||
|
||||
// Vérifier si des modifications sont demandées
|
||||
hasChanges := false
|
||||
@@ -254,12 +255,12 @@ func UpdateClientByAdmin(c *gin.Context) {
|
||||
|
||||
// Mise à jour du téléphone
|
||||
if req.Telephone != "" && req.Telephone != client.Telephone {
|
||||
if !validatePhoneNumber(req.Telephone) {
|
||||
if !utils.ValidatePhoneNumber(req.Telephone) {
|
||||
log.Printf("❌ [UPDATE_CLIENT_ADMIN] Téléphone invalide: %s", req.Telephone)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Numéro de téléphone invalide"})
|
||||
return
|
||||
}
|
||||
normalizedPhone := normalizePhoneNumber(req.Telephone)
|
||||
normalizedPhone := utils.NormalizePhoneNumber(req.Telephone)
|
||||
|
||||
// Vérifier que le téléphone n'est pas déjà utilisé
|
||||
if existingClient, _ := database.GetClientByTelephone(normalizedPhone); existingClient != nil && existingClient.ID != clientID {
|
||||
@@ -272,29 +273,12 @@ func UpdateClientByAdmin(c *gin.Context) {
|
||||
log.Printf("✏️ [UPDATE_CLIENT_ADMIN] Téléphone modifié: %s", normalizedPhone)
|
||||
}
|
||||
|
||||
// ✅ CORRECTION: Mise à jour du compteur de commandes (Admin uniquement)
|
||||
// Vérifier explicitement si le champ est présent (même si valeur = 0)
|
||||
if req.Command != nil && *req.Command != client.Command {
|
||||
client.Command = *req.Command
|
||||
hasChanges = true
|
||||
log.Printf("✏️ [UPDATE_CLIENT_ADMIN] Commandes modifiées: %d → %d", client.Command, *req.Command)
|
||||
}
|
||||
|
||||
// ✅ CORRECTION: Mise à jour des points weed/hash (Admin uniquement)
|
||||
if req.Point != nil && *req.Point != client.Point {
|
||||
client.Point = *req.Point
|
||||
hasChanges = true
|
||||
log.Printf("✏️ [UPDATE_CLIENT_ADMIN] Points Weed modifiés: %d → %d", client.Point, *req.Point)
|
||||
}
|
||||
|
||||
// ✅ CORRECTION CRITIQUE: Mise à jour des points zipette (Admin uniquement)
|
||||
if req.PointsZipette != nil && *req.PointsZipette != client.PointZipette {
|
||||
client.PointZipette = *req.PointsZipette
|
||||
hasChanges = true
|
||||
log.Printf("✏️ [UPDATE_CLIENT_ADMIN] Points Zipette modifiés: %d → %d", client.PointZipette, *req.PointsZipette)
|
||||
}
|
||||
|
||||
// ✅ CORRECTION: Mise à jour des amendes (Admin uniquement)
|
||||
if req.Amende != nil && *req.Amende != client.Amende {
|
||||
client.Amende = *req.Amende
|
||||
hasChanges = true
|
||||
@@ -310,9 +294,8 @@ func UpdateClientByAdmin(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// ✅ LOG DEBUG - État avant sauvegarde
|
||||
log.Printf("📊 [UPDATE_CLIENT_ADMIN] État avant save - Command: %d, Point: %d, PointZipette: %d, Amende: %.2f",
|
||||
client.Command, client.Point, client.PointZipette, client.Amende)
|
||||
log.Printf("📊 [UPDATE_CLIENT_ADMIN] État avant save - Command: %d, Amende: %.2f",
|
||||
client.Command, client.Amende)
|
||||
|
||||
// Sauvegarder les modifications
|
||||
if err := database.UpdateClient(client); err != nil {
|
||||
@@ -446,15 +429,14 @@ func UpdateUserByAdmin(c *gin.Context) {
|
||||
|
||||
func sanitizeClient(client *models.Client) gin.H {
|
||||
return gin.H{
|
||||
"id": client.ID,
|
||||
"username": client.Username,
|
||||
"nom": client.Nom,
|
||||
"prenom": client.Prenom,
|
||||
"telephone": client.Telephone,
|
||||
"command": client.Command,
|
||||
"point": client.Point,
|
||||
"points_zipette": client.PointZipette, // ✅ AJOUTÉ
|
||||
"amende": client.Amende,
|
||||
"id": client.ID,
|
||||
"username": client.Username,
|
||||
"nom": client.Nom,
|
||||
"prenom": client.Prenom,
|
||||
"telephone": client.Telephone,
|
||||
"command": client.Command,
|
||||
"points_extra": client.PointsExtra,
|
||||
"amende": client.Amende,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -352,6 +352,12 @@ func StartDelivery(c *gin.Context) {
|
||||
fmt.Sprintf("Livraison démarrée par %s", usernameStr),
|
||||
usernameStr)
|
||||
|
||||
// Notifier le client
|
||||
if clientUsername, _ := command["username"].(string); clientUsername != "" {
|
||||
msg := fmt.Sprintf("🛵 Votre commande #%d est en route !", commandID)
|
||||
database.NotifyClient(clientUsername, commandID, "en_route", msg)
|
||||
}
|
||||
|
||||
log.Printf("✅ [START] Livraison %d démarrée", commandID)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"gestion/db"
|
||||
"gestion/models"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
@@ -37,7 +37,7 @@ type zoneCheckResult struct {
|
||||
// Les zones sont lues depuis la DB (settings.PostalZones).
|
||||
// Code postal introuvable → OK = false (refus).
|
||||
// Code postal hors de toutes les zones → OK = false (refus).
|
||||
func checkDeliveryZone(deliveryAddress string, total float64, zones []db.PostalZone) zoneCheckResult {
|
||||
func checkDeliveryZone(deliveryAddress string, total float64, zones []models.PostalZone) zoneCheckResult {
|
||||
code := extractPostalCode(deliveryAddress)
|
||||
if code == "" {
|
||||
return zoneCheckResult{PostalCode: "", ZoneName: "inconnue", MinAmount: 0, OK: false}
|
||||
|
||||
Reference in New Issue
Block a user