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) }