Files
projet_gestion_commande/backend/gestion/handlers/redis_services.go
T
2026-06-14 17:50:35 +02:00

1059 lines
29 KiB
Go

package handlers
import (
"encoding/json"
"fmt"
"gestion/db"
"gestion/services"
"gestion/utils"
"log"
"net/http"
"slices"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
)
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)
}
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",
"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 {
utils.ServerErr(c, "Erreur lors de la mise à jour de la position", err)
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(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",
"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"})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"username": username,
"latitude": position.Latitude,
"longitude": position.Longitude,
"status": position.Status,
"updated_at": position.UpdatedAt,
})
}
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",
"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,
},
})
}
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 {
utils.BindErr(c, err)
return
}
validStatuses := []string{"available", "busy", "offline"}
if !slices.Contains(validStatuses, req.Status) {
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 {
utils.ServerErr(c, "Erreur lors de la mise à jour du statut", err)
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 {
utils.ServerErr(c, "Erreur récupération de la queue", err)
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"username": usernameStr,
"queue_info": queueInfo,
})
}
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
}
// Vérifier si amende == solde parrainage → compensation automatique
referralBalance, err := database.GetClientReferralBalance(req.Username)
if err == nil && float64(req.Points) == referralBalance && referralBalance > 0 {
if err := database.DebitReferralBalance(req.Username, referralBalance); err != nil {
utils.ServerErr(c, "Erreur débit solde parrainage", err)
return
}
log.Printf("🔄 [PENALTY] Compensation parrainage pour %s: amende %d€ annulée, solde parrainage %.2f€ débité par %s",
req.Username, req.Points, referralBalance, adminUsername)
c.JSON(http.StatusOK, gin.H{
"success": true,
"username": req.Username,
"compensated": true,
"message": "Amende annulée — solde parrainage débité en compensation",
})
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 {
utils.ServerErr(c, "Erreur lors de la récupération des pénalités", err)
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 {
utils.ServerErr(c, "Erreur lors de la récupération des pénalités", err)
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 {
utils.ServerErr(c, "Erreur lors de la récupération des clients", err)
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 {
utils.ServerErr(c, "Erreur lors de la récupération des statistiques", err)
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 {
Pool int `json:"pool"` // 0=pool principal, 1=pool secondaire, -1=tous (défaut)
}
req.Pool = -1 // défaut : reset total
if err := c.ShouldBindJSON(&req); err == nil {
// valeur reçue correctement
}
database := c.MustGet("database").(*db.Database)
extraPoolKey := ""
if req.Pool >= 0 {
if settings, err := database.GetSettings(); err == nil && req.Pool < len(settings.PointsPools) {
extraPoolKey = settings.PointsPools[req.Pool].Key
}
}
err := database.ResetClientPoint(username, req.Pool, extraPoolKey)
if err != nil {
utils.ServerErr(c, "Erreur lors de la réinitialisation", err)
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 {
utils.ServerErr(c, "Erreur lors de la réinitialisation", err)
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,
})
}
func AddClientPointsAdmin(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 {
PoolKey string `json:"pool_key" binding:"required"`
Points int `json:"points" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"})
return
}
if req.Points <= 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "Le nombre de points doit être positif"})
return
}
database := c.MustGet("database").(*db.Database)
// Vérifier que le pool_key existe dans les settings
settings, err := database.GetSettings()
if err != nil {
utils.ServerErr(c, "Erreur récupération paramètres", err)
return
}
poolExists := false
for _, pool := range settings.PointsPools {
if pool.Key == req.PoolKey {
poolExists = true
break
}
}
if !poolExists {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Pool de points invalide",
"pools_valides": func() []string {
keys := make([]string, 0, len(settings.PointsPools))
for _, p := range settings.PointsPools {
keys = append(keys, p.Key)
}
return keys
}(),
})
return
}
if err := database.AddClientPointsByCategory(username, req.Points, req.PoolKey); err != nil {
if strings.Contains(err.Error(), "non trouvé") {
c.JSON(http.StatusNotFound, gin.H{"error": "Client non trouvé"})
} else {
utils.ServerErr(c, "Erreur ajout de points", err)
}
return
}
log.Printf("✅ [ADD_POINTS] %d points (pool=%s) ajoutés à %s par %s",
req.Points, req.PoolKey, username, c.GetString("username"))
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Points ajoutés avec succès",
"username": username,
"pool_key": req.PoolKey,
"points": req.Points,
})
}
func SubtractClientPointsAdmin(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 {
PoolKey string `json:"pool_key" binding:"required"`
Points int `json:"points" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"})
return
}
if req.Points <= 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "Le nombre de points à retirer doit être positif"})
return
}
database := c.MustGet("database").(*db.Database)
settings, err := database.GetSettings()
if err != nil {
utils.ServerErr(c, "Erreur récupération paramètres", err)
return
}
poolExists := false
for _, pool := range settings.PointsPools {
if pool.Key == req.PoolKey {
poolExists = true
break
}
}
if !poolExists {
c.JSON(http.StatusBadRequest, gin.H{"error": "Pool de points invalide"})
return
}
// Récupérer le client pour vérifier le solde actuel
client, err := database.GetClientByUsername(username)
if err != nil || client == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Client non trouvé"})
return
}
current := client.PointsExtra[req.PoolKey]
// Plancher à 0
toSubtract := req.Points
if toSubtract > current {
toSubtract = current
}
if toSubtract == 0 {
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Aucun point à retirer (solde déjà à 0)",
"username": username,
"pool_key": req.PoolKey,
"points": 0,
})
return
}
if err := database.AddClientPointsByCategory(username, -toSubtract, req.PoolKey); err != nil {
utils.ServerErr(c, "Erreur retrait de points", err)
return
}
log.Printf("✅ [SUBTRACT_POINTS] %d points (pool=%s) retirés à %s par %s",
toSubtract, req.PoolKey, username, c.GetString("username"))
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Points retirés avec succès",
"username": username,
"pool_key": req.PoolKey,
"points": toSubtract,
})
}
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,
})
}
func refreshETAForActivDelivery(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)
}