chore: fix UI
This commit is contained in:
@@ -13,7 +13,6 @@ import (
|
|||||||
func (d *Database) CleanupInvalidQueueCommands() (int, error) {
|
func (d *Database) CleanupInvalidQueueCommands() (int, error) {
|
||||||
log.Println("🧹 [CLEANUP] Démarrage du nettoyage des commandes invalides...")
|
log.Println("🧹 [CLEANUP] Démarrage du nettoyage des commandes invalides...")
|
||||||
|
|
||||||
// Récupérer toutes les clés de commandes en attente
|
|
||||||
keys, err := Redis.Keys(RedisCtx, "queue:pending:*").Result()
|
keys, err := Redis.Keys(RedisCtx, "queue:pending:*").Result()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("erreur récupération des clés: %w", err)
|
return 0, fmt.Errorf("erreur récupération des clés: %w", err)
|
||||||
@@ -23,7 +22,6 @@ func (d *Database) CleanupInvalidQueueCommands() (int, error) {
|
|||||||
validCount := 0
|
validCount := 0
|
||||||
|
|
||||||
for _, key := range keys {
|
for _, key := range keys {
|
||||||
// Récupérer les données
|
|
||||||
data, err := Redis.Get(RedisCtx, key).Result()
|
data, err := Redis.Get(RedisCtx, key).Result()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("⚠️ [CLEANUP] Impossible de lire %s: %v", key, err)
|
log.Printf("⚠️ [CLEANUP] Impossible de lire %s: %v", key, err)
|
||||||
|
|||||||
@@ -81,6 +81,11 @@ func RegisterClient(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sanitize text inputs
|
||||||
|
req.Username = utils.StripHTML(req.Username)
|
||||||
|
req.Nom = utils.StripHTML(req.Nom)
|
||||||
|
req.Prenom = utils.StripHTML(req.Prenom)
|
||||||
|
|
||||||
// Validation téléphone
|
// Validation téléphone
|
||||||
if !utils.ValidatePhoneNumber(req.Telephone) {
|
if !utils.ValidatePhoneNumber(req.Telephone) {
|
||||||
log.Printf("❌ [REGISTER_CLIENT] Téléphone invalide: %s", req.Telephone)
|
log.Printf("❌ [REGISTER_CLIENT] Téléphone invalide: %s", req.Telephone)
|
||||||
@@ -180,6 +185,11 @@ func AdminCreateClient(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sanitize text inputs
|
||||||
|
req.Username = utils.StripHTML(req.Username)
|
||||||
|
req.Nom = utils.StripHTML(req.Nom)
|
||||||
|
req.Prenom = utils.StripHTML(req.Prenom)
|
||||||
|
|
||||||
if !utils.ValidatePhoneNumber(req.Telephone) {
|
if !utils.ValidatePhoneNumber(req.Telephone) {
|
||||||
log.Printf("❌ [ADMIN_CREATE_CLIENT] Téléphone invalide: %q", req.Telephone)
|
log.Printf("❌ [ADMIN_CREATE_CLIENT] Téléphone invalide: %q", req.Telephone)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Numéro de téléphone invalide"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Numéro de téléphone invalide"})
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ func IPNWebhook(c *gin.Context) {
|
|||||||
payAmount, _ := payload.PayAmount.Float64()
|
payAmount, _ := payload.PayAmount.Float64()
|
||||||
if err := database.UpdateCryptoPaymentStatus(payment.ID, payload.PaymentStatus, payAmount); err != nil {
|
if err := database.UpdateCryptoPaymentStatus(payment.ID, payload.PaymentStatus, payAmount); err != nil {
|
||||||
log.Printf("[IPN] erreur mise à jour paiement %d: %v", payment.ID, err)
|
log.Printf("[IPN] erreur mise à jour paiement %d: %v", payment.ID, err)
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "erreur base de données"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "erreur base de données"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -754,8 +754,26 @@ func ApplyClientPenalty(c *gin.Context) {
|
|||||||
return
|
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É
|
// ✅ APPLIQUER PÉNALITÉ
|
||||||
err := database.AddClientPenalty(req.Username, req.Points)
|
err = database.AddClientPenalty(req.Username, req.Points)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("❌ [PENALTY] Erreur: %v", err)
|
log.Printf("❌ [PENALTY] Erreur: %v", err)
|
||||||
|
|
||||||
@@ -1055,6 +1073,97 @@ 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) {
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// STATISTIQUES TEMPS RÉEL
|
// STATISTIQUES TEMPS RÉEL
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
@@ -50,13 +49,21 @@ func UpdateMyProfile(c *gin.Context) {
|
|||||||
hasChanges := false
|
hasChanges := false
|
||||||
|
|
||||||
// Mise à jour du username
|
// Mise à jour du username
|
||||||
|
if req.Username != "" {
|
||||||
|
req.Username = utils.StripHTML(req.Username)
|
||||||
|
}
|
||||||
if req.Username != "" && req.Username != client.Username {
|
if req.Username != "" && req.Username != client.Username {
|
||||||
// Vérifier que le nouveau username n'existe pas
|
// Vérifier que le nouveau username n'existe pas (clients et users)
|
||||||
if existingClient, _ := database.GetClientByUsername(req.Username); existingClient != nil {
|
if existingClient, _ := database.GetClientByUsername(req.Username); existingClient != nil {
|
||||||
log.Printf("❌ [UPDATE_MY_PROFILE] Username déjà utilisé: %s", req.Username)
|
log.Printf("❌ [UPDATE_MY_PROFILE] Username déjà utilisé: %s", req.Username)
|
||||||
c.JSON(http.StatusConflict, gin.H{"error": "Nom d'utilisateur déjà utilisé"})
|
c.JSON(http.StatusConflict, gin.H{"error": "Nom d'utilisateur déjà utilisé"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if existingUser, _ := database.GetUserByUsername(req.Username); existingUser != nil {
|
||||||
|
log.Printf("❌ [UPDATE_MY_PROFILE] Username réservé: %s", req.Username)
|
||||||
|
c.JSON(http.StatusConflict, gin.H{"error": "Nom d'utilisateur déjà utilisé"})
|
||||||
|
return
|
||||||
|
}
|
||||||
client.Username = req.Username
|
client.Username = req.Username
|
||||||
hasChanges = true
|
hasChanges = true
|
||||||
}
|
}
|
||||||
@@ -78,22 +85,28 @@ func UpdateMyProfile(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Mise à jour du nom
|
// Mise à jour du nom
|
||||||
|
if req.Nom != "" {
|
||||||
|
req.Nom = utils.StripHTML(req.Nom)
|
||||||
|
}
|
||||||
if req.Nom != "" && req.Nom != client.Nom {
|
if req.Nom != "" && req.Nom != client.Nom {
|
||||||
if len(strings.TrimSpace(req.Nom)) < 2 {
|
if len(req.Nom) < 2 {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Le nom doit contenir au moins 2 caractères"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Le nom doit contenir au moins 2 caractères"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
client.Nom = strings.TrimSpace(req.Nom)
|
client.Nom = req.Nom
|
||||||
hasChanges = true
|
hasChanges = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mise à jour du prénom
|
// Mise à jour du prénom
|
||||||
|
if req.Prenom != "" {
|
||||||
|
req.Prenom = utils.StripHTML(req.Prenom)
|
||||||
|
}
|
||||||
if req.Prenom != "" && req.Prenom != client.Prenom {
|
if req.Prenom != "" && req.Prenom != client.Prenom {
|
||||||
if len(strings.TrimSpace(req.Prenom)) < 2 {
|
if len(req.Prenom) < 2 {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Le prénom doit contenir au moins 2 caractères"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Le prénom doit contenir au moins 2 caractères"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
client.Prenom = strings.TrimSpace(req.Prenom)
|
client.Prenom = req.Prenom
|
||||||
hasChanges = true
|
hasChanges = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,6 +221,9 @@ func UpdateClientByAdmin(c *gin.Context) {
|
|||||||
hasChanges := false
|
hasChanges := false
|
||||||
|
|
||||||
// Mise à jour du username
|
// Mise à jour du username
|
||||||
|
if req.Username != "" {
|
||||||
|
req.Username = utils.StripHTML(req.Username)
|
||||||
|
}
|
||||||
if req.Username != "" && req.Username != client.Username {
|
if req.Username != "" && req.Username != client.Username {
|
||||||
// Vérifier que le nouveau username n'existe pas
|
// Vérifier que le nouveau username n'existe pas
|
||||||
if existingClient, _ := database.GetClientByUsername(req.Username); existingClient != nil {
|
if existingClient, _ := database.GetClientByUsername(req.Username); existingClient != nil {
|
||||||
@@ -238,15 +254,21 @@ func UpdateClientByAdmin(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Mise à jour du nom
|
// Mise à jour du nom
|
||||||
|
if req.Nom != "" {
|
||||||
|
req.Nom = utils.StripHTML(req.Nom)
|
||||||
|
}
|
||||||
if req.Nom != "" && req.Nom != client.Nom {
|
if req.Nom != "" && req.Nom != client.Nom {
|
||||||
client.Nom = strings.TrimSpace(req.Nom)
|
client.Nom = req.Nom
|
||||||
hasChanges = true
|
hasChanges = true
|
||||||
log.Printf("✏️ [UPDATE_CLIENT_ADMIN] Nom modifié: %s", req.Nom)
|
log.Printf("✏️ [UPDATE_CLIENT_ADMIN] Nom modifié: %s", req.Nom)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mise à jour du prénom
|
// Mise à jour du prénom
|
||||||
|
if req.Prenom != "" {
|
||||||
|
req.Prenom = utils.StripHTML(req.Prenom)
|
||||||
|
}
|
||||||
if req.Prenom != "" && req.Prenom != client.Prenom {
|
if req.Prenom != "" && req.Prenom != client.Prenom {
|
||||||
client.Prenom = strings.TrimSpace(req.Prenom)
|
client.Prenom = req.Prenom
|
||||||
hasChanges = true
|
hasChanges = true
|
||||||
log.Printf("✏️ [UPDATE_CLIENT_ADMIN] Prénom modifié: %s", req.Prenom)
|
log.Printf("✏️ [UPDATE_CLIENT_ADMIN] Prénom modifié: %s", req.Prenom)
|
||||||
}
|
}
|
||||||
@@ -277,11 +299,17 @@ func UpdateClientByAdmin(c *gin.Context) {
|
|||||||
log.Printf("✏️ [UPDATE_CLIENT_ADMIN] Commandes modifiées: %d → %d", client.Command, *req.Command)
|
log.Printf("✏️ [UPDATE_CLIENT_ADMIN] Commandes modifiées: %d → %d", client.Command, *req.Command)
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Amende != nil && *req.Amende != client.Amende {
|
if req.Amende != nil {
|
||||||
|
if *req.Amende < 0 {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "L'amende ne peut pas être négative"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if *req.Amende != client.Amende {
|
||||||
client.Amende = *req.Amende
|
client.Amende = *req.Amende
|
||||||
hasChanges = true
|
hasChanges = true
|
||||||
log.Printf("✏️ [UPDATE_CLIENT_ADMIN] Amendes modifiées: %.2f → %.2f", client.Amende, *req.Amende)
|
log.Printf("✏️ [UPDATE_CLIENT_ADMIN] Amendes modifiées: %.2f → %.2f", client.Amende, *req.Amende)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !hasChanges {
|
if !hasChanges {
|
||||||
log.Printf("ℹ️ [UPDATE_CLIENT_ADMIN] Aucune modification détectée pour client ID=%d", clientID)
|
log.Printf("ℹ️ [UPDATE_CLIENT_ADMIN] Aucune modification détectée pour client ID=%d", clientID)
|
||||||
@@ -353,13 +381,21 @@ func UpdateUserByAdmin(c *gin.Context) {
|
|||||||
hasChanges := false
|
hasChanges := false
|
||||||
|
|
||||||
// Mise à jour du username
|
// Mise à jour du username
|
||||||
|
if req.Username != "" {
|
||||||
|
req.Username = utils.StripHTML(req.Username)
|
||||||
|
}
|
||||||
if req.Username != "" && req.Username != user.Username {
|
if req.Username != "" && req.Username != user.Username {
|
||||||
// Vérifier que le nouveau username n'existe pas
|
// Vérifier que le nouveau username n'existe pas (users et clients)
|
||||||
if existingUser, _ := database.GetUserByUsername(req.Username); existingUser != nil {
|
if existingUser, _ := database.GetUserByUsername(req.Username); existingUser != nil {
|
||||||
log.Printf("❌ [UPDATE_USER_ADMIN] Username déjà utilisé: %s", req.Username)
|
log.Printf("❌ [UPDATE_USER_ADMIN] Username déjà utilisé: %s", req.Username)
|
||||||
c.JSON(http.StatusConflict, gin.H{"error": "Nom d'utilisateur déjà utilisé"})
|
c.JSON(http.StatusConflict, gin.H{"error": "Nom d'utilisateur déjà utilisé"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if existingClient, _ := database.GetClientByUsername(req.Username); existingClient != nil {
|
||||||
|
log.Printf("❌ [UPDATE_USER_ADMIN] Username réservé: %s", req.Username)
|
||||||
|
c.JSON(http.StatusConflict, gin.H{"error": "Nom d'utilisateur déjà utilisé"})
|
||||||
|
return
|
||||||
|
}
|
||||||
user.Username = req.Username
|
user.Username = req.Username
|
||||||
hasChanges = true
|
hasChanges = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -479,10 +479,8 @@ func RateLimitMiddleware(c *gin.Context) {
|
|||||||
// LoginRateLimitMiddleware limite les tentatives de connexion par IP.
|
// LoginRateLimitMiddleware limite les tentatives de connexion par IP.
|
||||||
// Config: 10 tentatives par 15 minutes.
|
// Config: 10 tentatives par 15 minutes.
|
||||||
func LoginRateLimitMiddleware(c *gin.Context) {
|
func LoginRateLimitMiddleware(c *gin.Context) {
|
||||||
ip := c.GetHeader("X-Real-IP")
|
|
||||||
if ip == "" {
|
ip := c.ClientIP()
|
||||||
ip = c.ClientIP()
|
|
||||||
}
|
|
||||||
|
|
||||||
rateLimitKey := "ratelimit:login:" + ip
|
rateLimitKey := "ratelimit:login:" + ip
|
||||||
|
|
||||||
|
|||||||
@@ -251,6 +251,7 @@ func SetupRoutes(router *gin.Engine, database *db.Database, geoService *services
|
|||||||
adminGroupV2.POST("/client/:username/penalties/reset", handlers.ResetClientPenaltiesAdmin) // Reset amende
|
adminGroupV2.POST("/client/:username/penalties/reset", handlers.ResetClientPenaltiesAdmin) // Reset amende
|
||||||
adminGroupV2.POST("/client/:username/point/reset", handlers.ResetClientPointAdmin) // Reset points → 0
|
adminGroupV2.POST("/client/:username/point/reset", handlers.ResetClientPointAdmin) // Reset points → 0
|
||||||
adminGroupV2.POST("/client/:username/points/add", handlers.AddClientPointsAdmin) // Ajouter points par pool
|
adminGroupV2.POST("/client/:username/points/add", handlers.AddClientPointsAdmin) // Ajouter points par pool
|
||||||
|
adminGroupV2.POST("/client/:username/points/subtract", handlers.SubtractClientPointsAdmin) // Enlever points par pool
|
||||||
adminGroupV2.GET("/penalties/all", handlers.GetAllClientsWithPenalties) // Liste clients avec pénalités
|
adminGroupV2.GET("/penalties/all", handlers.GetAllClientsWithPenalties) // Liste clients avec pénalités
|
||||||
adminGroupV2.GET("/penalties/stats", handlers.GetPenaltiesStats)
|
adminGroupV2.GET("/penalties/stats", handlers.GetPenaltiesStats)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"html"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var htmlTagRe = regexp.MustCompile(`<[^>]*>`)
|
||||||
|
|
||||||
|
// StripHTML removes HTML tags and decodes HTML entities from s.
|
||||||
|
func StripHTML(s string) string {
|
||||||
|
stripped := htmlTagRe.ReplaceAllString(s, "")
|
||||||
|
decoded := html.UnescapeString(stripped)
|
||||||
|
return StripCRLF(decoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
func StripCRLF(s string) string {
|
||||||
|
return strings.TrimSpace(strings.Map(func(r rune) rune {
|
||||||
|
if r == '\r' || r == '\n' {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}, s))
|
||||||
|
}
|
||||||
@@ -564,14 +564,14 @@ export const applyClientPenalty = async (
|
|||||||
username: string,
|
username: string,
|
||||||
points: number,
|
points: number,
|
||||||
reason: string,
|
reason: string,
|
||||||
): Promise<{ success: boolean; error?: string }> => {
|
): Promise<{ success: boolean; compensated?: boolean; error?: string }> => {
|
||||||
try {
|
try {
|
||||||
await apiClient.post(`${V2}/admin/protected/penalty`, {
|
const { data } = await apiClient.post(`${V2}/admin/protected/penalty`, {
|
||||||
username,
|
username,
|
||||||
points,
|
points,
|
||||||
reason,
|
reason,
|
||||||
});
|
});
|
||||||
return { success: true };
|
return { success: true, compensated: data?.compensated === true };
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
return { success: false, error: e.response?.data?.error || "Erreur" };
|
return { success: false, error: e.response?.data?.error || "Erreur" };
|
||||||
}
|
}
|
||||||
@@ -623,6 +623,22 @@ export const addClientPoints = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const subtractClientPoints = async (
|
||||||
|
username: string,
|
||||||
|
poolKey: string,
|
||||||
|
points: number,
|
||||||
|
): Promise<{ success: boolean; error?: string }> => {
|
||||||
|
try {
|
||||||
|
await apiClient.post(
|
||||||
|
`${V2}/admin/protected/client/${username}/points/subtract`,
|
||||||
|
{ pool_key: poolKey, points },
|
||||||
|
);
|
||||||
|
return { success: true };
|
||||||
|
} catch (e: any) {
|
||||||
|
return { success: false, error: e.response?.data?.error || "Erreur" };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// ALERTES
|
// ALERTES
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|||||||
@@ -513,12 +513,13 @@ export default function ProductsScreen() {
|
|||||||
backgroundColor: "rgba(0,0,0,0.85)",
|
backgroundColor: "rgba(0,0,0,0.85)",
|
||||||
justifyContent: "flex-end",
|
justifyContent: "flex-end",
|
||||||
},
|
},
|
||||||
modalWrapper: { maxHeight: "94%" },
|
modalWrapper: { maxHeight: "94%", flex: 1 },
|
||||||
modal: {
|
modal: {
|
||||||
backgroundColor: colors.bgSecondary,
|
backgroundColor: colors.bgSecondary,
|
||||||
borderTopLeftRadius: 24,
|
borderTopLeftRadius: 24,
|
||||||
borderTopRightRadius: 24,
|
borderTopRightRadius: 24,
|
||||||
maxHeight: "100%",
|
maxHeight: "100%",
|
||||||
|
flex: 1,
|
||||||
},
|
},
|
||||||
modalHeader: {
|
modalHeader: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
@@ -538,6 +539,7 @@ export default function ProductsScreen() {
|
|||||||
modalBody: {
|
modalBody: {
|
||||||
paddingHorizontal: spacing.xl,
|
paddingHorizontal: spacing.xl,
|
||||||
paddingTop: spacing.l,
|
paddingTop: spacing.l,
|
||||||
|
flex: 1,
|
||||||
},
|
},
|
||||||
modalFooter: {
|
modalFooter: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
resetClientPenalties,
|
resetClientPenalties,
|
||||||
resetClientPoints,
|
resetClientPoints,
|
||||||
addClientPoints,
|
addClientPoints,
|
||||||
|
subtractClientPoints,
|
||||||
} from "../../api/api_admin";
|
} from "../../api/api_admin";
|
||||||
import type { CancelledOrder } from "../../api/api_admin";
|
import type { CancelledOrder } from "../../api/api_admin";
|
||||||
import type { ClientResponse } from "../../api/types";
|
import type { ClientResponse } from "../../api/types";
|
||||||
@@ -197,6 +198,8 @@ export default function UsersScreen() {
|
|||||||
const [amendeLoading, setAmendeLoading] = useState(false);
|
const [amendeLoading, setAmendeLoading] = useState(false);
|
||||||
const [pointsInputs, setPointsInputs] = useState<Record<string, string>>({});
|
const [pointsInputs, setPointsInputs] = useState<Record<string, string>>({});
|
||||||
const [pointsLoading, setPointsLoading] = useState<Record<string, boolean>>({});
|
const [pointsLoading, setPointsLoading] = useState<Record<string, boolean>>({});
|
||||||
|
const [subtractInputs, setSubtractInputs] = useState<Record<string, string>>({});
|
||||||
|
const [subtractLoading, setSubtractLoading] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
// Cancelled orders modal
|
// Cancelled orders modal
|
||||||
const [cancelledModal, setCancelledModal] = useState<{
|
const [cancelledModal, setCancelledModal] = useState<{
|
||||||
@@ -206,7 +209,7 @@ export default function UsersScreen() {
|
|||||||
loading: boolean;
|
loading: boolean;
|
||||||
}>({ visible: false, username: "", orders: [], loading: false });
|
}>({ visible: false, username: "", orders: [], loading: false });
|
||||||
|
|
||||||
const { alert, showError, showConfirm, hideAlert } = useAlert();
|
const { alert, showError, showSuccess, showConfirm, hideAlert } = useAlert();
|
||||||
|
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
// Data
|
// Data
|
||||||
@@ -455,11 +458,13 @@ export default function UsersScreen() {
|
|||||||
setAmendeReason("");
|
setAmendeReason("");
|
||||||
setPointsInputs({});
|
setPointsInputs({});
|
||||||
setPointsLoading({});
|
setPointsLoading({});
|
||||||
|
setSubtractInputs({});
|
||||||
|
setSubtractLoading({});
|
||||||
setSanctionTab("amende");
|
setSanctionTab("amende");
|
||||||
setSanctionModal({ visible: true, client });
|
setSanctionModal({ visible: true, client });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddAmende = async () => {
|
const handleAddAmende = () => {
|
||||||
const pts = parseInt(amendeAmount, 10);
|
const pts = parseInt(amendeAmount, 10);
|
||||||
if (isNaN(pts) || pts <= 0) {
|
if (isNaN(pts) || pts <= 0) {
|
||||||
showError("Erreur", "Entrez un montant valide");
|
showError("Erreur", "Entrez un montant valide");
|
||||||
@@ -470,13 +475,22 @@ export default function UsersScreen() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!sanctionModal.client) return;
|
if (!sanctionModal.client) return;
|
||||||
|
showConfirm(
|
||||||
|
"Confirmer l'amende",
|
||||||
|
`Ajouter ${pts} € d'amende à "${sanctionModal.client.username}" ?\nRaison : ${amendeReason.trim()}`,
|
||||||
|
async () => {
|
||||||
setAmendeLoading(true);
|
setAmendeLoading(true);
|
||||||
const res = await applyClientPenalty(sanctionModal.client.username, pts, amendeReason.trim());
|
const res = await applyClientPenalty(sanctionModal.client!.username, pts, amendeReason.trim());
|
||||||
setAmendeLoading(false);
|
setAmendeLoading(false);
|
||||||
if (!res.success) { showError("Erreur", res.error || "Echec"); return; }
|
if (!res.success) { showError("Erreur", res.error || "Echec"); return; }
|
||||||
setAmendeAmount("");
|
setAmendeAmount("");
|
||||||
setAmendeReason("");
|
setAmendeReason("");
|
||||||
|
if (res.compensated) {
|
||||||
|
showSuccess("Compensation", "Amende annulée — solde parrainage débité en compensation");
|
||||||
|
}
|
||||||
await loadData();
|
await loadData();
|
||||||
|
},
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleResetAmende = () => {
|
const handleResetAmende = () => {
|
||||||
@@ -493,19 +507,46 @@ export default function UsersScreen() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddPoints = async (poolKey: string) => {
|
const handleAddPoints = (poolKey: string, poolName: string) => {
|
||||||
const pts = parseInt(pointsInputs[poolKey] || "", 10);
|
const pts = parseInt(pointsInputs[poolKey] || "", 10);
|
||||||
if (isNaN(pts) || pts <= 0) {
|
if (isNaN(pts) || pts <= 0) {
|
||||||
showError("Erreur", "Entrez un nombre de points valide");
|
showError("Erreur", "Entrez un nombre de points valide");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!sanctionModal.client) return;
|
if (!sanctionModal.client) return;
|
||||||
|
showConfirm(
|
||||||
|
"Confirmer l'ajout",
|
||||||
|
`Ajouter ${pts} points "${poolName}" à "${sanctionModal.client.username}" ?`,
|
||||||
|
async () => {
|
||||||
setPointsLoading((prev) => ({ ...prev, [poolKey]: true }));
|
setPointsLoading((prev) => ({ ...prev, [poolKey]: true }));
|
||||||
const res = await addClientPoints(sanctionModal.client.username, poolKey, pts);
|
const res = await addClientPoints(sanctionModal.client!.username, poolKey, pts);
|
||||||
setPointsLoading((prev) => ({ ...prev, [poolKey]: false }));
|
setPointsLoading((prev) => ({ ...prev, [poolKey]: false }));
|
||||||
if (!res.success) { showError("Erreur", res.error || "Echec"); return; }
|
if (!res.success) { showError("Erreur", res.error || "Echec"); return; }
|
||||||
setPointsInputs((prev) => ({ ...prev, [poolKey]: "" }));
|
setPointsInputs((prev) => ({ ...prev, [poolKey]: "" }));
|
||||||
await loadData();
|
await loadData();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubtractPoints = (poolKey: string, poolName: string) => {
|
||||||
|
const pts = parseInt(subtractInputs[poolKey] || "", 10);
|
||||||
|
if (isNaN(pts) || pts <= 0) {
|
||||||
|
showError("Erreur", "Entrez un nombre de points valide");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!sanctionModal.client) return;
|
||||||
|
showConfirm(
|
||||||
|
"Confirmer le retrait",
|
||||||
|
`Retirer ${pts} points "${poolName}" à "${sanctionModal.client.username}" ?`,
|
||||||
|
async () => {
|
||||||
|
setSubtractLoading((prev) => ({ ...prev, [poolKey]: true }));
|
||||||
|
const res = await subtractClientPoints(sanctionModal.client!.username, poolKey, pts);
|
||||||
|
setSubtractLoading((prev) => ({ ...prev, [poolKey]: false }));
|
||||||
|
if (!res.success) { showError("Erreur", res.error || "Echec"); return; }
|
||||||
|
setSubtractInputs((prev) => ({ ...prev, [poolKey]: "" }));
|
||||||
|
await loadData();
|
||||||
|
},
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleResetPoints = (poolKey: string, poolName: string, poolIdx: number) => {
|
const handleResetPoints = (poolKey: string, poolName: string, poolIdx: number) => {
|
||||||
@@ -1538,7 +1579,7 @@ export default function UsersScreen() {
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => handleAddPoints(key)}
|
onPress={() => handleAddPoints(key, name)}
|
||||||
disabled={pointsLoading[key]}
|
disabled={pointsLoading[key]}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: color + "20",
|
backgroundColor: color + "20",
|
||||||
@@ -1562,6 +1603,43 @@ export default function UsersScreen() {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* Input + bouton enlever */}
|
||||||
|
<View style={{ flexDirection: "row", gap: spacing.s, alignItems: "center" }}>
|
||||||
|
<View style={{ flex: 1 }}>
|
||||||
|
<TextInput
|
||||||
|
placeholder="Points à enlever"
|
||||||
|
value={subtractInputs[key] ?? ""}
|
||||||
|
onChangeText={(v) =>
|
||||||
|
setSubtractInputs((prev) => ({ ...prev, [key]: v }))
|
||||||
|
}
|
||||||
|
keyboardType="numeric"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => handleSubtractPoints(key, name)}
|
||||||
|
disabled={subtractLoading[key]}
|
||||||
|
style={{
|
||||||
|
backgroundColor: colors.danger + "15",
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.danger,
|
||||||
|
borderRadius: borderRadius.md,
|
||||||
|
paddingHorizontal: spacing.m,
|
||||||
|
paddingVertical: spacing.m,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
minWidth: 72,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{subtractLoading[key] ? (
|
||||||
|
<ActivityIndicator size="small" color={colors.danger} />
|
||||||
|
) : (
|
||||||
|
<Text style={{ color: colors.danger, fontWeight: "700", fontSize: fontSize.sm }}>
|
||||||
|
− Enlever
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
{/* Bouton reset */}
|
{/* Bouton reset */}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => handleResetPoints(key, name, idx)}
|
onPress={() => handleResetPoints(key, name, idx)}
|
||||||
|
|||||||
Reference in New Issue
Block a user