1142 lines
31 KiB
Go
1142 lines
31 KiB
Go
// ============================================
|
|
// handlers/redis_handlers.go - VERSION FINALE
|
|
// UTILISE UNIQUEMENT LES MÉTHODES DB PostgreSQL
|
|
// ============================================
|
|
|
|
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"gestion/db"
|
|
"gestion/services"
|
|
"log"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// ============================================
|
|
// GESTION DE LA FILE DE COMMANDES
|
|
// ============================================
|
|
|
|
func validatePenaltyPoints(points int) error {
|
|
if points <= 0 {
|
|
return fmt.Errorf("points invalides: %d (doit être > 0)", points)
|
|
}
|
|
if points > 1000 {
|
|
return fmt.Errorf("points trop élevés: %d (max 1000)", points)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func sanitizeReason(reason string) string {
|
|
if len(reason) > 500 {
|
|
reason = reason[:500]
|
|
}
|
|
reason = strings.Map(func(r rune) rune {
|
|
if r < 32 || r == 127 {
|
|
return -1
|
|
}
|
|
return r
|
|
}, 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 {
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"error": "Erreur lors de l'assignation automatique",
|
|
"details": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"message": "Commande assignée automatiquement",
|
|
"command_id": nextCommand.CommandID,
|
|
})
|
|
}
|
|
|
|
// ============================================
|
|
// GESTION DES LIVREURS - LOCALISATION
|
|
// ============================================
|
|
|
|
// UpdateLivreurLocation met à jour la position GPS du livreur
|
|
// POST /api/v1/livreur/location/update
|
|
// Body: {"latitude": 48.8566, "longitude": 2.3522}
|
|
func UpdateLivreurLocation(c *gin.Context) {
|
|
database := c.MustGet("database").(*db.Database)
|
|
|
|
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
|
|
}
|
|
|
|
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": "Données invalides - latitude et longitude requises",
|
|
"details": err.Error(),
|
|
"format": gin.H{
|
|
"latitude": "number (required)",
|
|
"longitude": "number (required)",
|
|
},
|
|
})
|
|
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
|
|
}
|
|
|
|
usernameStr := username.(string)
|
|
|
|
// ✅ 1. Mettre à jour la position GPS dans Redis
|
|
err := database.UpdateDeliveryPersonLocation(usernameStr, req.Latitude, req.Longitude)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"error": "Erreur lors de la mise à jour de la position",
|
|
"details": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
log.Printf("📍 Position GPS mise à jour pour %s: (%.6f, %.6f)",
|
|
usernameStr, req.Latitude, req.Longitude)
|
|
|
|
// ✅ Recalculer l'ETA en temps réel si livreur en_route
|
|
go refreshETAForActivDelivery(database, usernameStr, req.Latitude, req.Longitude)
|
|
|
|
// ✅ 2. Vérifier/Initialiser le statut du livreur
|
|
statusKey := fmt.Sprintf("delivery:status:%s", usernameStr)
|
|
statusData, err := db.Redis.Get(db.RedisCtx, statusKey).Result()
|
|
|
|
if err != nil || statusData == "" {
|
|
// ✅ Aucun statut existant → Créer un statut "available" par défaut
|
|
log.Printf("🆕 [INIT_STATUS] Création statut 'available' pour %s", usernameStr)
|
|
database.SetDeliveryPersonStatus(usernameStr, "available", 0)
|
|
} else {
|
|
// ✅ Statut existe → Vérifier s'il est "offline"
|
|
var status map[string]interface{}
|
|
json.Unmarshal([]byte(statusData), &status)
|
|
|
|
if currentStatus, ok := status["status"].(string); ok && currentStatus == "offline" {
|
|
// Si le livreur était offline et envoie sa position, le remettre available
|
|
log.Printf("🔄 [REACTIVATE] %s passe de 'offline' à 'available'", usernameStr)
|
|
database.SetDeliveryPersonStatus(usernameStr, "available", 0)
|
|
} else {
|
|
// ✅ Synchroniser le statut basé sur la queue
|
|
go database.UpdateDeliverymanStatusBasedOnQueue(usernameStr)
|
|
}
|
|
}
|
|
|
|
// ✅ 3. Récupérer le statut final
|
|
statusData, _ = db.Redis.Get(db.RedisCtx, statusKey).Result()
|
|
var currentStatus string = "available"
|
|
if statusData != "" {
|
|
var status map[string]interface{}
|
|
json.Unmarshal([]byte(statusData), &status)
|
|
if s, ok := status["status"].(string); ok {
|
|
currentStatus = s
|
|
}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"message": "Position GPS mise à jour avec succès",
|
|
"username": usernameStr,
|
|
"latitude": req.Latitude,
|
|
"longitude": req.Longitude,
|
|
"status": currentStatus,
|
|
"timestamp": time.Now().Unix(),
|
|
})
|
|
}
|
|
|
|
// GetMyLocation récupère la position actuelle du livreur connecté
|
|
// GET /api/v1/livreur/location
|
|
func GetMyLocation(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
|
|
}
|
|
|
|
usernameStr := username.(string)
|
|
|
|
// Récupérer la position depuis Redis
|
|
position, err := database.GetLivreurPosition(usernameStr)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{
|
|
"error": "Position non disponible",
|
|
"details": err.Error(),
|
|
"message": "Veuillez d'abord mettre à jour votre position",
|
|
})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"username": usernameStr,
|
|
"latitude": position.Latitude,
|
|
"longitude": position.Longitude,
|
|
"status": position.Status,
|
|
"updated_at": position.UpdatedAt,
|
|
})
|
|
}
|
|
|
|
// GetDeliveryPersonLocation récupère la position d'un livreur (Admin seulement)
|
|
// GET /api/v2/admin/protected/delivery/:username/location
|
|
func GetDeliveryPersonLocation(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é"})
|
|
return
|
|
}
|
|
|
|
username := c.Param("username")
|
|
if username == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Username requis"})
|
|
return
|
|
}
|
|
|
|
position, err := database.GetLivreurPosition(username)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{
|
|
"error": "Position non trouvée pour ce livreur",
|
|
"details": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"username": username,
|
|
"latitude": position.Latitude,
|
|
"longitude": position.Longitude,
|
|
"status": position.Status,
|
|
"updated_at": position.UpdatedAt,
|
|
})
|
|
}
|
|
|
|
// ============================================
|
|
// LOCALISATION DU LIVREUR POUR UNE COMMANDE
|
|
// ============================================
|
|
|
|
// GetDeliverymanLocationForCommand récupère la position GPS du livreur assigné à une commande
|
|
// GET /api/v2/admin/protected/commands/:id/deliveryman/location (ADMIN)
|
|
// GET /api/v1/cabine/commands/:id/deliveryman/location (CABINE)
|
|
// Accessible uniquement par les admins et la cabine
|
|
func GetDeliverymanLocationForCommand(c *gin.Context) {
|
|
database := c.MustGet("database").(*db.Database)
|
|
|
|
// Vérifier l'authentification
|
|
username, exists := c.Get("username")
|
|
if !exists {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
|
|
return
|
|
}
|
|
|
|
userRole := c.GetString("role")
|
|
|
|
// ✅ Vérifier que l'utilisateur est admin ou cabine
|
|
if userRole != "admin" && userRole != "cabine" {
|
|
c.JSON(http.StatusForbidden, gin.H{
|
|
"error": "Accès refusé - Réservé aux administrateurs et à la cabine",
|
|
})
|
|
return
|
|
}
|
|
|
|
// Récupérer l'ID de la commande
|
|
commandID, err := strconv.Atoi(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "ID de commande invalide"})
|
|
return
|
|
}
|
|
|
|
usernameStr := username.(string)
|
|
|
|
// ✅ 1. Récupérer la commande depuis PostgreSQL
|
|
command, err := database.GetCommandByID(commandID)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{
|
|
"error": "Commande non trouvée",
|
|
})
|
|
return
|
|
}
|
|
|
|
// ✅ 2. Récupérer les informations de la commande
|
|
commandUsername, _ := command["username"].(string)
|
|
commandStatus, _ := command["status"].(string)
|
|
|
|
// ✅ 3. 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 n'est encore assigné à cette commande",
|
|
"message": "La commande est en attente d'assignation",
|
|
"command_info": gin.H{
|
|
"command_id": commandID,
|
|
"client": commandUsername,
|
|
"status": commandStatus,
|
|
},
|
|
})
|
|
return
|
|
}
|
|
|
|
// ✅ 4. Récupérer la position GPS du livreur depuis Redis
|
|
lat, lon, err := database.GetDeliveryPersonLocation(livreurAssign)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{
|
|
"error": "Position du livreur non disponible",
|
|
"details": err.Error(),
|
|
"message": "Le livreur n'a pas encore partagé sa position",
|
|
"command_info": gin.H{
|
|
"command_id": commandID,
|
|
"client": commandUsername,
|
|
"deliveryman": livreurAssign,
|
|
"status": commandStatus,
|
|
},
|
|
})
|
|
return
|
|
}
|
|
|
|
// ✅ 5. Récupérer le statut du livreur depuis Redis
|
|
statusKey := fmt.Sprintf("delivery:status:%s", livreurAssign)
|
|
statusData, err := db.Redis.Get(db.RedisCtx, statusKey).Result()
|
|
|
|
var deliverymanStatus string = "unknown"
|
|
var currentCommand int = 0
|
|
if err == nil && statusData != "" {
|
|
var status map[string]interface{}
|
|
json.Unmarshal([]byte(statusData), &status)
|
|
if s, ok := status["status"].(string); ok {
|
|
deliverymanStatus = s
|
|
}
|
|
if cmd, ok := status["current_command"].(float64); ok {
|
|
currentCommand = int(cmd)
|
|
}
|
|
}
|
|
|
|
// ✅ 6. Récupérer l'ETA de la commande depuis Redis (si disponible)
|
|
etaKey := fmt.Sprintf("command:eta:%d", commandID)
|
|
etaData, _ := db.Redis.Get(db.RedisCtx, etaKey).Result()
|
|
|
|
var etaMinutes int = 0
|
|
var etaSetAt int64 = 0
|
|
if etaData != "" {
|
|
var eta map[string]interface{}
|
|
json.Unmarshal([]byte(etaData), &eta)
|
|
if minutes, ok := eta["minutes"].(float64); ok {
|
|
etaMinutes = int(minutes)
|
|
}
|
|
if timestamp, ok := eta["set_at"].(float64); ok {
|
|
etaSetAt = int64(timestamp)
|
|
}
|
|
}
|
|
|
|
// ✅ 7. Calculer le temps écoulé depuis la dernière mise à jour
|
|
locationKey := fmt.Sprintf("delivery:location:%s", livreurAssign)
|
|
locationData, _ := db.Redis.Get(db.RedisCtx, locationKey).Result()
|
|
|
|
var lastUpdate int64 = 0
|
|
var isRecentUpdate bool = false
|
|
if locationData != "" {
|
|
var location map[string]interface{}
|
|
json.Unmarshal([]byte(locationData), &location)
|
|
if timestamp, ok := location["last_update"].(float64); ok {
|
|
lastUpdate = int64(timestamp)
|
|
// Position considérée comme récente si < 5 minutes
|
|
isRecentUpdate = (time.Now().Unix() - lastUpdate) < 300
|
|
}
|
|
}
|
|
|
|
// ✅ 8. Récupérer la queue du livreur pour contexte
|
|
queueKey := fmt.Sprintf("queue:deliveryman:%s", livreurAssign)
|
|
queueSize, _ := db.Redis.ZCard(db.RedisCtx, queueKey).Result()
|
|
|
|
log.Printf("📍 [LOCATION] %s (%s) a récupéré la position du livreur %s pour commande #%d: (%.6f, %.6f)",
|
|
usernameStr, userRole, livreurAssign, commandID, lat, lon)
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"data": gin.H{
|
|
"command_id": commandID,
|
|
"client": commandUsername,
|
|
"command_status": commandStatus,
|
|
"deliveryman": gin.H{
|
|
"username": livreurAssign,
|
|
"status": deliverymanStatus,
|
|
"current_command": currentCommand,
|
|
"queue_size": int(queueSize),
|
|
"location": gin.H{
|
|
"latitude": lat,
|
|
"longitude": lon,
|
|
"last_update": lastUpdate,
|
|
"last_update_ago": time.Now().Unix() - lastUpdate,
|
|
"is_recent": isRecentUpdate,
|
|
},
|
|
},
|
|
"eta": gin.H{
|
|
"minutes": etaMinutes,
|
|
"has_eta": etaMinutes > 0,
|
|
"set_at": etaSetAt,
|
|
},
|
|
},
|
|
"requested_by": gin.H{
|
|
"username": usernameStr,
|
|
"role": userRole,
|
|
},
|
|
})
|
|
}
|
|
|
|
// ============================================
|
|
// GESTION DES LIVREURS - STATUT
|
|
// ============================================
|
|
|
|
// UpdateDeliveryPersonStatus met à jour le statut de disponibilité du livreur
|
|
// POST /api/v1/livreur/status
|
|
// Body: {"status": "available" | "busy" | "offline"}
|
|
func UpdateDeliveryPersonStatus(c *gin.Context) {
|
|
database := c.MustGet("database").(*db.Database)
|
|
|
|
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
|
|
}
|
|
|
|
var req struct {
|
|
Status string `json:"status" binding:"required"`
|
|
}
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{
|
|
"error": "Données invalides",
|
|
"details": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
// Validation du statut
|
|
validStatuses := []string{"available", "busy", "offline"}
|
|
isValid := false
|
|
for _, s := range validStatuses {
|
|
if req.Status == s {
|
|
isValid = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !isValid {
|
|
c.JSON(http.StatusBadRequest, gin.H{
|
|
"error": "Statut invalide",
|
|
"valid_statuses": validStatuses,
|
|
"received": req.Status,
|
|
})
|
|
return
|
|
}
|
|
|
|
usernameStr := username.(string)
|
|
|
|
err := database.SetDeliveryPersonStatus(usernameStr, req.Status, 0)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"error": "Erreur lors de la mise à jour du statut",
|
|
"details": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
log.Printf("🔄 Statut mis à jour pour %s: %s", usernameStr, req.Status)
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"message": "Statut mis à jour avec succès",
|
|
"username": usernameStr,
|
|
"status": req.Status,
|
|
"timestamp": time.Now().Unix(),
|
|
})
|
|
}
|
|
|
|
// GetMyStatus récupère le statut actuel du livreur connecté
|
|
// GET /api/v1/livreur/status
|
|
func GetMyStatus(c *gin.Context) {
|
|
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
|
|
}
|
|
|
|
usernameStr := username.(string)
|
|
|
|
// Récupérer le statut depuis Redis
|
|
key := "delivery:status:" + usernameStr
|
|
statusData, err := db.Redis.Get(db.RedisCtx, key).Result()
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{
|
|
"error": "Statut non disponible",
|
|
"message": "Veuillez d'abord définir votre statut",
|
|
})
|
|
return
|
|
}
|
|
|
|
var status map[string]interface{}
|
|
if err := json.Unmarshal([]byte(statusData), &status); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"error": "Erreur de décodage du statut",
|
|
})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"username": usernameStr,
|
|
"status": status,
|
|
})
|
|
}
|
|
|
|
// GetMyQueue récupère la queue de livraisons du livreur connecté
|
|
// GET /api/v1/livreur/queue
|
|
func GetMyQueue(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
|
|
}
|
|
|
|
usernameStr := username.(string)
|
|
|
|
queueInfo, err := database.GetDeliverymanQueueInfo(usernameStr)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"error": "Erreur récupération de la queue",
|
|
"details": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"username": usernameStr,
|
|
"queue_info": queueInfo,
|
|
})
|
|
}
|
|
|
|
// 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 {
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"error": "Erreur lors de la récupération des livreurs",
|
|
"details": err.Error(),
|
|
})
|
|
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 {
|
|
c.JSON(http.StatusBadRequest, gin.H{
|
|
"error": "Données invalides",
|
|
"details": err.Error(),
|
|
})
|
|
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 {
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"error": "Erreur lors de la mise à jour de l'ETA",
|
|
"details": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
log.Printf("⏱️ ETA défini pour commande %d par %s: %d minutes", commandID, usernameStr, req.ETAMinutes)
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"message": "ETA mis à jour avec succès",
|
|
"command_id": commandID,
|
|
"eta_minutes": req.ETAMinutes,
|
|
})
|
|
}
|
|
|
|
// ============================================
|
|
// PÉNALITÉS - UTILISE PostgreSQL
|
|
// ============================================
|
|
|
|
// ApplyClientPenalty applique une pénalité à un client (Admin seulement)
|
|
// POST /api/v2/admin/protected/penalty
|
|
// Body: {"username": "john", "points": 50, "reason": "Retard paiement"}
|
|
func ApplyClientPenalty(c *gin.Context) {
|
|
database := c.MustGet("database").(*db.Database)
|
|
|
|
if c.GetString("role") != "admin" {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"})
|
|
return
|
|
}
|
|
|
|
adminUsername, _ := safeGetUsername(c)
|
|
|
|
var req struct {
|
|
Username string `json:"username" binding:"required"`
|
|
Points int `json:"points" binding:"required"`
|
|
Reason string `json:"reason" binding:"required"`
|
|
}
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"})
|
|
return
|
|
}
|
|
|
|
// ✅ VALIDATION
|
|
if len(req.Username) > 100 || req.Username == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Username invalide"})
|
|
return
|
|
}
|
|
|
|
if err := validatePenaltyPoints(req.Points); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
req.Reason = sanitizeReason(req.Reason)
|
|
if req.Reason == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Raison requise"})
|
|
return
|
|
}
|
|
|
|
// ✅ APPLIQUER PÉNALITÉ
|
|
err := database.AddClientPenalty(req.Username, req.Points)
|
|
if err != nil {
|
|
log.Printf("❌ [PENALTY] Erreur: %v", err)
|
|
|
|
if strings.Contains(err.Error(), "non trouvé") {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Client non trouvé"})
|
|
} else {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur application pénalité"})
|
|
}
|
|
return
|
|
}
|
|
|
|
log.Printf("⚠️ Pénalité appliquée à %s: %d points par %s - %s",
|
|
req.Username, req.Points, adminUsername, req.Reason)
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"username": req.Username,
|
|
"points": req.Points,
|
|
})
|
|
}
|
|
|
|
// GetMyPenalties récupère les pénalités du client connecté
|
|
// GET /api/v1/client/penalties
|
|
func GetMyPenalties(c *gin.Context) {
|
|
username, exists := c.Get("username")
|
|
if !exists {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Non authentifié"})
|
|
return
|
|
}
|
|
|
|
userRole := c.GetString("role")
|
|
if userRole != "client" {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "Accès réservé aux clients"})
|
|
return
|
|
}
|
|
|
|
database := c.MustGet("database").(*db.Database)
|
|
usernameStr := username.(string)
|
|
|
|
// ✅ UTILISE LA MÉTHODE DÉDIÉE GetClientPenaltiesInfo
|
|
penaltiesInfo, err := database.GetClientPenaltiesInfo(usernameStr)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"error": "Erreur lors de la récupération des pénalités",
|
|
"details": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"data": penaltiesInfo,
|
|
})
|
|
}
|
|
|
|
// ============================================
|
|
// ⚠️ ENDPOINT SUPPRIMÉ: PayMyPenalties
|
|
// Les pénalités sont maintenant automatiquement remises à zéro
|
|
// lors de l'application d'une nouvelle pénalité
|
|
// ============================================
|
|
|
|
// GetClientPenaltiesAdmin récupère les pénalités d'un client (Admin seulement)
|
|
// GET /api/v2/admin/protected/client/:username/penalties
|
|
func GetClientPenaltiesAdmin(c *gin.Context) {
|
|
userRole := c.GetString("role")
|
|
if userRole != "admin" && userRole != "cabine" {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"})
|
|
return
|
|
}
|
|
|
|
username := c.Param("username")
|
|
if username == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Username requis"})
|
|
return
|
|
}
|
|
|
|
database := c.MustGet("database").(*db.Database)
|
|
|
|
// ✅ UTILISE GetClientPenaltiesInfo
|
|
penaltiesInfo, err := database.GetClientPenaltiesInfo(username)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"error": "Erreur lors de la récupération des pénalités",
|
|
"details": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"data": penaltiesInfo,
|
|
})
|
|
}
|
|
|
|
// GetAllClientsWithPenalties récupère tous les clients ayant des pénalités (Admin seulement)
|
|
// GET /api/v2/admin/protected/penalties/all
|
|
func GetAllClientsWithPenalties(c *gin.Context) {
|
|
userRole := c.GetString("role")
|
|
if userRole != "admin" {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"})
|
|
return
|
|
}
|
|
|
|
database := c.MustGet("database").(*db.Database)
|
|
|
|
// ✅ UTILISE GetAllClientsWithPenalties
|
|
clients, err := database.GetAllClientsWithPenalties()
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"error": "Erreur lors de la récupération des clients",
|
|
"details": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"data": gin.H{
|
|
"clients": clients,
|
|
"count": len(clients),
|
|
},
|
|
})
|
|
}
|
|
|
|
// GetPenaltiesStats récupère les statistiques globales sur les pénalités (Admin seulement)
|
|
// GET /api/v2/admin/protected/penalties/stats
|
|
func GetPenaltiesStats(c *gin.Context) {
|
|
userRole := c.GetString("role")
|
|
if userRole != "admin" && userRole != "cabine" {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"})
|
|
return
|
|
}
|
|
|
|
database := c.MustGet("database").(*db.Database)
|
|
|
|
// ✅ UTILISE GetClientPenaltiesStats
|
|
stats, err := database.GetClientPenaltiesStats()
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"error": "Erreur lors de la récupération des statistiques",
|
|
"details": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"data": stats,
|
|
})
|
|
}
|
|
|
|
func ResetClientPointAdmin(c *gin.Context) {
|
|
userRole := c.GetString("role")
|
|
if userRole != "admin" && userRole != "cabine" {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"})
|
|
return
|
|
}
|
|
|
|
username := c.Param("username")
|
|
if username == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Username requis"})
|
|
return
|
|
}
|
|
var req struct {
|
|
ResetCancellationsPoints bool `json:"reset_cancellations_points"`
|
|
}
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
req.ResetCancellationsPoints = false
|
|
}
|
|
|
|
database := c.MustGet("database").(*db.Database)
|
|
|
|
err := database.ResetClientPoint(username, req.ResetCancellationsPoints)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"error": "Erreur lors de la réinitialisation",
|
|
"details": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "Points réinitialisés"})
|
|
}
|
|
|
|
// ResetClientPenaltiesAdmin réinitialise les pénalités d'un client (Admin seulement)
|
|
func ResetClientPenaltiesAdmin(c *gin.Context) {
|
|
userRole := c.GetString("role")
|
|
if userRole != "admin" && userRole != "cabine" {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"})
|
|
return
|
|
}
|
|
|
|
username := c.Param("username")
|
|
if username == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Username requis"})
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
ResetCancellationsCount bool `json:"reset_cancellations_count"`
|
|
}
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
// Par défaut, ne pas réinitialiser le compteur d'annulations
|
|
req.ResetCancellationsCount = false
|
|
}
|
|
|
|
database := c.MustGet("database").(*db.Database)
|
|
|
|
// ✅ UTILISE ResetClientPenalties
|
|
err := database.ResetClientPenalties(username, req.ResetCancellationsCount)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"error": "Erreur lors de la réinitialisation",
|
|
"details": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
log.Printf("🔄 Pénalités réinitialisées pour %s (reset_count=%v)", username, req.ResetCancellationsCount)
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"message": "Pénalités réinitialisées avec succès",
|
|
"username": username,
|
|
"reset_cancellations_count": req.ResetCancellationsCount,
|
|
})
|
|
}
|
|
|
|
// ============================================
|
|
// STATISTIQUES TEMPS RÉEL
|
|
// ============================================
|
|
|
|
// GetRealtimeStats récupère les statistiques en temps réel
|
|
// GET /api/v2/admin/protected/stats/realtime
|
|
func GetRealtimeStats(c *gin.Context) {
|
|
userRole := c.GetString("role")
|
|
if userRole != "admin" {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"})
|
|
return
|
|
}
|
|
|
|
stats, err := db.Redis.HGetAll(db.RedisCtx, "stats:realtime").Result()
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{
|
|
"error": "Erreur lors de la récupération des statistiques",
|
|
})
|
|
return
|
|
}
|
|
|
|
if len(stats) == 0 {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"message": "Aucune statistique disponible pour le moment",
|
|
"stats": map[string]interface{}{},
|
|
})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"stats": stats,
|
|
})
|
|
}
|
|
|
|
// ============================================
|
|
// 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
|
|
statusKey := fmt.Sprintf("delivery:status:%s", username)
|
|
statusData, err := db.Redis.Get(db.RedisCtx, statusKey).Result()
|
|
if err != nil || statusData == "" {
|
|
return
|
|
}
|
|
|
|
var status map[string]interface{}
|
|
if err := json.Unmarshal([]byte(statusData), &status); err != nil {
|
|
return
|
|
}
|
|
|
|
// 2. Seulement si en_route ou arrived
|
|
currentStatus, _ := status["status"].(string)
|
|
if currentStatus != "en_route" && currentStatus != "arrived" {
|
|
return
|
|
}
|
|
|
|
// 3. Récupérer la commande active
|
|
var commandID int
|
|
switch v := status["current_command"].(type) {
|
|
case float64:
|
|
commandID = int(v)
|
|
case int:
|
|
commandID = v
|
|
default:
|
|
return
|
|
}
|
|
if commandID <= 0 {
|
|
return
|
|
}
|
|
|
|
// 4. Récupérer les coordonnées destination depuis le cache Redis
|
|
destCacheKey := fmt.Sprintf("command:destination:%d", commandID)
|
|
destData, err := db.Redis.Get(db.RedisCtx, destCacheKey).Result()
|
|
if err != nil || destData == "" {
|
|
return
|
|
}
|
|
|
|
var coords struct {
|
|
Lat float64 `json:"lat"`
|
|
Lon float64 `json:"lon"`
|
|
}
|
|
if err := json.Unmarshal([]byte(destData), &coords); err != nil || coords.Lat == 0 {
|
|
return
|
|
}
|
|
|
|
// 5. Calculer l'ETA depuis la position GPS actuelle
|
|
from := services.Coordinates{Latitude: lat, Longitude: lon}
|
|
to := services.Coordinates{Latitude: coords.Lat, Longitude: coords.Lon}
|
|
|
|
etaMinutes, distanceKm, err := services.GetETAWithTraffic(from, to)
|
|
if err != nil {
|
|
// Fallback Haversine uniquement si TomTom indisponible
|
|
distanceKm = services.CalculateDistance(from, to)
|
|
etaMinutes = services.CalculateETA(distanceKm)
|
|
log.Printf("⚠️ [ETA_REALTIME] TomTom indisponible pour %s cmd %d, fallback: %.2fkm → %dmin",
|
|
username, commandID, distanceKm, etaMinutes)
|
|
} else {
|
|
log.Printf("🔄 [ETA_REALTIME] %s cmd %d recalculé: %.2fkm → %dmin (TomTom)",
|
|
username, commandID, distanceKm, etaMinutes)
|
|
}
|
|
|
|
// 6. Mettre à jour le cache Redis ETA (écrase l'ancien)
|
|
now := time.Now()
|
|
arrivalTime := now.Add(time.Duration(etaMinutes) * time.Minute)
|
|
etaKey := fmt.Sprintf("command:eta:%d", commandID)
|
|
|
|
db.Redis.HSet(db.RedisCtx, etaKey, map[string]interface{}{
|
|
"command_id": commandID,
|
|
"eta_minutes": etaMinutes,
|
|
"updated_at": now.Unix(),
|
|
"arrival_time": arrivalTime.Unix(),
|
|
"distance_km": distanceKm,
|
|
"with_traffic": err == nil,
|
|
})
|
|
db.Redis.Expire(db.RedisCtx, etaKey, 4*time.Hour)
|
|
}
|