From 446b15d29cc31dac1d3fd90c3fca84c2408fd76c Mon Sep 17 00:00:00 2001 From: Xor290 Date: Sat, 13 Jun 2026 14:23:23 +0200 Subject: [PATCH] chore: build --- backend/gestion/db/db_basket.go | 45 ++++++++++++++++++- backend/gestion/db/db_init.go | 5 +++ backend/gestion/handlers/client_tracking.go | 1 + backend/gestion/handlers/panier.go | 14 ++++++ backend/gestion/handlers/points.go | 17 +++++++ backend/gestion/models/panier.go | 1 + backend/gestion/models/settings.go | 10 +++-- frontend-prep/src/api/api.ts | 7 +++ frontend-prep/src/api/api_types.ts | 1 + frontend-prep/src/pages/User/Cart.tsx | 17 ++++++- frontend-prep/src/pages/User/Checkout.tsx | 4 +- .../src/pages/User/ConsultationHistorique.tsx | 5 ++- .../src/pages/User/SuiviLivraison.tsx | 44 +++++++++--------- 13 files changed, 137 insertions(+), 34 deletions(-) diff --git a/backend/gestion/db/db_basket.go b/backend/gestion/db/db_basket.go index 371c2d40..684c1835 100644 --- a/backend/gestion/db/db_basket.go +++ b/backend/gestion/db/db_basket.go @@ -31,7 +31,7 @@ func (d *Database) GetProductPrice(name, category string, quantity float64) (flo func (d *Database) GetAllProductsInBasket(username string) ([]models.Panier, error) { var baskets []models.Panier err := d.GDB.Raw(` - SELECT b.id, b.username, b.product_id, b.quantity, b.price, b.created_at, + SELECT b.id, b.username, b.product_id, b.quantity, b.price, b.is_reward, b.created_at, p.name as product_name, p.category, p.description FROM baskets b INNER JOIN products p ON b.product_id = p.id @@ -43,6 +43,47 @@ func (d *Database) GetAllProductsInBasket(username string) ([]models.Panier, err return baskets, nil } +// AddRewardToBasket ajoute un produit récompense au panier (prix = 0, is_reward = true). +// Pas de vérification de stock — les récompenses sont gérées par l'admin. +func (d *Database) AddRewardToBasket(username string, productID int, quantity float64) (*models.Panier, error) { + var basket models.Panier + err := d.GDB.Transaction(func(tx *gorm.DB) error { + // Vérifier que le produit existe + var productName string + if err := tx.Raw(`SELECT name FROM products WHERE id = ?`, productID).Scan(&productName).Error; err != nil || productName == "" { + return fmt.Errorf("produit récompense introuvable (id=%d)", productID) + } + // Supprimer tout article récompense existant pour ce produit (remplacement) + tx.Exec(`DELETE FROM baskets WHERE username = ? AND product_id = ? AND is_reward = true`, username, productID) + // Insérer avec prix 0 et is_reward = true + return tx.Raw(` + INSERT INTO baskets (username, product_id, quantity, price, is_reward, created_at) + VALUES (?, ?, ?, 0, true, CURRENT_TIMESTAMP) + RETURNING id, username, product_id, quantity, price, is_reward, created_at`, + username, productID, quantity).Scan(&basket).Error + }) + if err != nil { + return nil, err + } + return &basket, nil +} + +// HasOnlyRewardItems retourne true si le panier ne contient que des articles récompense. +func (d *Database) HasOnlyRewardItems(username string) (bool, error) { + var counts struct { + Total int `gorm:"column:total"` + Normal int `gorm:"column:normal"` + } + err := d.GDB.Raw(` + SELECT COUNT(*) as total, + COUNT(*) FILTER (WHERE is_reward = false) as normal + FROM baskets WHERE username = ?`, username).Scan(&counts).Error + if err != nil { + return false, err + } + return counts.Total > 0 && counts.Normal == 0, nil +} + // AddToBasket vérifie le stock disponible et ajoute l'article au panier. // Le stock n'est pas décrémenté ici — il l'est uniquement au checkout. func (d *Database) AddToBasket(username string, productID int, quantity float64) (*models.Panier, error) { @@ -186,7 +227,7 @@ func (d *Database) GetReservedQuantityInBaskets(productID int) (float64, error) func (d *Database) GetBasketItems(username string) ([]map[string]any, error) { var items []map[string]any - if err := d.GDB.Raw(`SELECT product_id, quantity::float8 as quantity, price::float8 as price FROM baskets WHERE username = ?`, + if err := d.GDB.Raw(`SELECT product_id, quantity::float8 as quantity, price::float8 as price, is_reward FROM baskets WHERE username = ?`, username).Scan(&items).Error; err != nil { return nil, err } diff --git a/backend/gestion/db/db_init.go b/backend/gestion/db/db_init.go index e70a1c55..3d72303a 100644 --- a/backend/gestion/db/db_init.go +++ b/backend/gestion/db/db_init.go @@ -120,6 +120,11 @@ func InitDB() *Database { log.Fatalf("❌ Erreur migration baskets.quantity: %v", err) } + // Migration: baskets.is_reward — marquer les articles issus d'une récompense points + if _, err = database.Exec(`ALTER TABLE baskets ADD COLUMN IF NOT EXISTS is_reward BOOLEAN NOT NULL DEFAULT FALSE`); err != nil { + log.Fatalf("❌ Erreur migration baskets.is_reward: %v", err) + } + // Migration: command_items.quantite INTEGER → NUMERIC(10,3) pour supporter les quantités fractionnaires if _, err = database.Exec(` DO $$ diff --git a/backend/gestion/handlers/client_tracking.go b/backend/gestion/handlers/client_tracking.go index 7bee7851..ddaf52a4 100644 --- a/backend/gestion/handlers/client_tracking.go +++ b/backend/gestion/handlers/client_tracking.go @@ -123,6 +123,7 @@ func GetMyCommandsWithTracking(c *gin.Context) { "status_message": getStatusMessage(cmd["status"].(string)), "adresse": cmd["adresse"], "total_prix": cmd["total_prix"], + "referral_used": cmd["referral_used"], "created_at": cmd["created_at"], "livreur": livreurInfo, "eta": etaData, diff --git a/backend/gestion/handlers/panier.go b/backend/gestion/handlers/panier.go index da8d9b80..7fc6f506 100644 --- a/backend/gestion/handlers/panier.go +++ b/backend/gestion/handlers/panier.go @@ -302,6 +302,20 @@ func ValidateBasket(c *gin.Context) { return } + // Vérifier qu'il y a au moins un article normal (non-récompense) + hasNormalItem := false + for _, item := range items { + if isReward, ok := item["is_reward"].(bool); !ok || !isReward { + hasNormalItem = true + break + } + } + if !hasNormalItem { + log.Printf("❌ [CHECKOUT] Panier contient uniquement des récompenses pour %s", usernameStr) + c.JSON(http.StatusBadRequest, gin.H{"error": "Vous devez commander au moins un produit de la boutique pour bénéficier de votre récompense"}) + return + } + log.Printf("🛒 [CHECKOUT] Panier: %d articles", len(items)) // ============================================ diff --git a/backend/gestion/handlers/points.go b/backend/gestion/handlers/points.go index 2b945949..bad61396 100644 --- a/backend/gestion/handlers/points.go +++ b/backend/gestion/handlers/points.go @@ -3,6 +3,7 @@ package handlers import ( "gestion/db" "gestion/utils" + "log" "net/http" "strings" @@ -191,10 +192,26 @@ func ClaimMyReward(c *gin.Context) { return } + // Ajouter le produit récompense au panier si configuré + productAdded := false + var productName string + if reward.RewardProductID > 0 && reward.RewardQuantity > 0 { + qty := reward.RewardQuantity + if item, addErr := database.AddRewardToBasket(username, reward.RewardProductID, qty); addErr == nil { + productAdded = true + productName = item.ProductName + log.Printf("✅ [CLAIM] Produit récompense id=%d (%.2f) ajouté au panier de %s", reward.RewardProductID, qty, username) + } else { + log.Printf("⚠️ [CLAIM] Impossible d'ajouter produit récompense: %v", addErr) + } + } + c.JSON(http.StatusOK, gin.H{ "success": true, "description": reward.Description, "remaining_rewards": remaining, + "product_added": productAdded, + "product_name": productName, }) } diff --git a/backend/gestion/models/panier.go b/backend/gestion/models/panier.go index 6e47601a..24e4c21a 100644 --- a/backend/gestion/models/panier.go +++ b/backend/gestion/models/panier.go @@ -11,6 +11,7 @@ type Panier struct { Description string `json:"description"` Quantity float64 `json:"quantity"` Price float64 `json:"price"` + IsReward bool `json:"is_reward"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at,omitempty"` } diff --git a/backend/gestion/models/settings.go b/backend/gestion/models/settings.go index a15b7f7c..097169d8 100644 --- a/backend/gestion/models/settings.go +++ b/backend/gestion/models/settings.go @@ -24,10 +24,12 @@ type RewardCategoryConfig struct { // PointsReward représente la récompense débloquée à partir d'un seuil de points cumulés type PointsReward struct { - Threshold int `json:"threshold"` // points cumulés nécessaires (ex: 20) - Type string `json:"type"` // "free_product" | "half_price_product" | "custom" - Description string `json:"description"` // description libre affichée au client - CategoryConfigs []RewardCategoryConfig `json:"category_configs"` // catégories + produits éligibles + Threshold int `json:"threshold"` // points cumulés nécessaires (ex: 20) + Type string `json:"type"` // "free_product" | "half_price_product" | "custom" + Description string `json:"description"` // description libre affichée au client + CategoryConfigs []RewardCategoryConfig `json:"category_configs"` // catégories + produits éligibles + RewardProductID int `json:"reward_product_id"` // ID produit ajouté au panier (0 = désactivé) + RewardQuantity float64 `json:"reward_quantity"` // quantité du produit récompense } // DaySchedule représente les horaires de livraison pour un jour de la semaine diff --git a/frontend-prep/src/api/api.ts b/frontend-prep/src/api/api.ts index 51fa076d..62a48de1 100644 --- a/frontend-prep/src/api/api.ts +++ b/frontend-prep/src/api/api.ts @@ -718,6 +718,9 @@ export const createCheckout = async (checkoutData: CheckoutData) => { pay_currency: data.pay_currency as string | undefined, price_amount: data.price_amount as number | undefined, price_currency: data.price_currency as string | undefined, + referral_used: data.referral_used as number | undefined, + referral_balance: data.referral_balance as number | undefined, + client_order_number: data.client_order_number as number | undefined, }; } catch (error) { console.error("❌ [CHECKOUT] Erreur:", error); @@ -2194,6 +2197,8 @@ export const claimMyReward = async (poolKey: string): Promise<{ success: boolean; description?: string; remaining_rewards?: number; + product_added?: boolean; + product_name?: string; error?: string; }> => { const token = getAuthToken(); @@ -2213,6 +2218,8 @@ export const claimMyReward = async (poolKey: string): Promise<{ success: true, description: data.description, remaining_rewards: data.remaining_rewards, + product_added: data.product_added, + product_name: data.product_name, }; } catch { return { success: false, error: "Erreur de connexion" }; diff --git a/frontend-prep/src/api/api_types.ts b/frontend-prep/src/api/api_types.ts index dc29c41f..a671d763 100644 --- a/frontend-prep/src/api/api_types.ts +++ b/frontend-prep/src/api/api_types.ts @@ -110,6 +110,7 @@ export interface CartItem { quantity: number; category: string; image?: string; + is_reward?: boolean; } /** diff --git a/frontend-prep/src/pages/User/Cart.tsx b/frontend-prep/src/pages/User/Cart.tsx index 57516a7a..27e182a2 100644 --- a/frontend-prep/src/pages/User/Cart.tsx +++ b/frontend-prep/src/pages/User/Cart.tsx @@ -176,9 +176,22 @@ function Cart() { {/* Infos */}
-

{item.name_product}

+

+ {item.name_product} + {item.is_reward && ( + + 🎁 Récompense + + )} +

{item.quantity}g

-

{item.price.toFixed(2)} €

+

+ {item.is_reward ? ( + Offert + ) : ( + `${item.price.toFixed(2)} €` + )} +

{/* Bouton supprimer */} diff --git a/frontend-prep/src/pages/User/Checkout.tsx b/frontend-prep/src/pages/User/Checkout.tsx index dea2895b..5b00ccf2 100644 --- a/frontend-prep/src/pages/User/Checkout.tsx +++ b/frontend-prep/src/pages/User/Checkout.tsx @@ -355,13 +355,13 @@ function Checkout() { // ✅ Préparer les données pour le modal setConfirmationData({ command_id, - client_order_number: (response as Record).client_order_number as number | undefined, + client_order_number: response.client_order_number, assigned_to, queue_info, delivery_address: delivery_address || address, arrivalTime, total: frontendTotal, - referral_used: (response as Record).referral_used as number | undefined, + referral_used: response.referral_used, clientInfo: { first_name: firstName, last_name: lastName, diff --git a/frontend-prep/src/pages/User/ConsultationHistorique.tsx b/frontend-prep/src/pages/User/ConsultationHistorique.tsx index 6f95e722..ca1f30cf 100644 --- a/frontend-prep/src/pages/User/ConsultationHistorique.tsx +++ b/frontend-prep/src/pages/User/ConsultationHistorique.tsx @@ -176,7 +176,10 @@ function ConsultationHistorique() { const res = await claimMyReward(poolKey); setClaimingPool(null); if (res.success) { - setClaimFeedback({ pool: poolKey, type: "success", text: res.description || "Récompense réclamée !" }); + const text = res.product_added && res.product_name + ? `🎁 ${res.product_name} ajouté à votre panier ! Commandez au moins un produit pour en profiter.` + : res.description || "Récompense réclamée !"; + setClaimFeedback({ pool: poolKey, type: "success", text }); getMyPointsRewards().then((r) => { if (r.success) setPointsRewards(r); }); } else { setClaimFeedback({ pool: poolKey, type: "error", text: res.error || "Erreur" }); diff --git a/frontend-prep/src/pages/User/SuiviLivraison.tsx b/frontend-prep/src/pages/User/SuiviLivraison.tsx index e3b04007..300f3326 100644 --- a/frontend-prep/src/pages/User/SuiviLivraison.tsx +++ b/frontend-prep/src/pages/User/SuiviLivraison.tsx @@ -73,30 +73,21 @@ interface ToastMessage { * Somme des prix individuels (pas de multiplication) */ const getTotalAmount = (order: OrderWithTracking): number => { - // 1. Priorité: champ total stocké en DB + let gross = 0; + if (typeof order.total === "number" && order.total > 0) { - return order.total; - } - - // 2. Fallback: total_prix - if (typeof order.total_prix === "number" && order.total_prix > 0) { - return order.total_prix; - } - - // 3. Calcul depuis items (comme dans Checkout: somme des prix) - if (order.items && order.items.length > 0) { - const calculatedTotal = order.items.reduce((sum, item) => { + gross = order.total; + } else if (typeof order.total_prix === "number" && order.total_prix > 0) { + gross = order.total_prix; + } else if (order.items && order.items.length > 0) { + gross = order.items.reduce((sum, item) => { const itemPrice = item.prix || item.price || 0; - return sum + itemPrice; // ✅ Somme simple (pas de × quantity) + return sum + itemPrice; }, 0); - - console.log( - `💰 [getTotalAmount] Commande ${order.id}: ${calculatedTotal.toFixed(2)}€`, - ); - return calculatedTotal; } - return 0; + const referralUsed = typeof order.referral_used === "number" ? order.referral_used : 0; + return Math.max(0, gross - referralUsed); }; const getClientInfo = (order: OrderWithTracking) => { @@ -933,14 +924,21 @@ function SuiviLivraison() { />{" "} Montant total + {(order.referral_used ?? 0) > 0 && ( +

+ Brut : {(order.total_prix ?? 0).toFixed(2)} € +

+ )}

- {getTotalAmount( - order, - ).toFixed(2)}{" "} - € + {getTotalAmount(order).toFixed(2)} €

+ {(order.referral_used ?? 0) > 0 && ( +

+ — dont {(order.referral_used!).toFixed(2)} € parrainage déduit +

+ )}