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