diff --git a/frontend-admin/src/screens/admin/SettingsScreen.tsx b/frontend-admin/src/screens/admin/SettingsScreen.tsx index 0fb7af5d..112a8f31 100644 --- a/frontend-admin/src/screens/admin/SettingsScreen.tsx +++ b/frontend-admin/src/screens/admin/SettingsScreen.tsx @@ -1161,13 +1161,22 @@ function PromotionProductPicker({ }); }; - const updateProductQuantity = (id: number, quantity: number) => { - onChange({ - ...catConfig, - products: catConfig.products.map((pq) => - pq.product_id === id ? { ...pq, quantity } : pq - ), - }); + // Chaque produit peut être en promo sur plusieurs paliers de quantité en + // même temps (ex: 1g ET 3g) — on ajoute/retire l'entrée {product_id, + // quantity} correspondante plutôt que de remplacer une quantité unique. + const toggleProductQuantity = (id: number, quantity: number) => { + const exists = catConfig.products.some((pq) => pq.product_id === id && pq.quantity === quantity); + if (exists) { + onChange({ + ...catConfig, + products: catConfig.products.filter((pq) => !(pq.product_id === id && pq.quantity === quantity)), + }); + } else { + onChange({ + ...catConfig, + products: [...catConfig.products, { product_id: id, quantity }], + }); + } }; return ( @@ -1227,8 +1236,9 @@ function PromotionProductPicker({ )} - {/* Mode "Sélection" : chaque produit choisi a sa propre quantité, - via les paliers de prix réels du produit (pas de saisie libre) */} + {/* Mode "Sélection" : chaque produit choisi peut être en promo sur + plusieurs paliers de quantité à la fois, via les paliers de + prix réels du produit (pas de saisie libre) */} {!catConfig.all_products && ( {catProducts.length === 0 ? ( @@ -1263,13 +1273,16 @@ function PromotionProductPicker({ {catConfig.products.length > 0 && ( - {catConfig.products.map((pq) => { - const prod = catProducts.find((p) => p.id === pq.product_id); + {Array.from(new Set(catConfig.products.map((pq) => pq.product_id))).map((productId) => { + const prod = catProducts.find((p) => p.id === productId); const tiers = (prod?.prices ?? []).filter((pr) => pr.active_price !== false); + const selectedQuantities = catConfig.products + .filter((pq) => pq.product_id === productId) + .map((pq) => pq.quantity); return ( - + - {prod?.name ?? `Produit #${pq.product_id}`} + {prod?.name ?? `Produit #${productId}`} {tiers.length === 0 ? ( @@ -1277,11 +1290,11 @@ function PromotionProductPicker({ Aucun palier de prix actif pour ce produit ) : tiers.map((tier) => { - const isSel = pq.quantity === tier.quantity; + const isSel = selectedQuantities.includes(tier.quantity); return ( updateProductQuantity(pq.product_id, tier.quantity)} + onPress={() => toggleProductQuantity(productId, tier.quantity)} style={{ paddingHorizontal: spacing.s, paddingVertical: 3, borderRadius: borderRadius.sm, borderWidth: 1.5, diff --git a/frontend-prep/src/components/ProductCard.css b/frontend-prep/src/components/ProductCard.css index 23b4ae1a..c27a5729 100644 --- a/frontend-prep/src/components/ProductCard.css +++ b/frontend-prep/src/components/ProductCard.css @@ -122,6 +122,13 @@ text-align: center; } +.product-price-strike { + color: var(--text-muted); + text-decoration: line-through; + font-size: 0.75em; + font-weight: 500; +} + .product-stock { color: var(--text-muted); font-size: clamp(0.85rem, 2.5vw, 1rem); diff --git a/frontend-prep/src/components/ProductCard.tsx b/frontend-prep/src/components/ProductCard.tsx index 09ed2843..7170cdd1 100644 --- a/frontend-prep/src/components/ProductCard.tsx +++ b/frontend-prep/src/components/ProductCard.tsx @@ -28,7 +28,13 @@ interface ProductCardProps { image: string; stock: number; category: string; - prices?: Array<{ quantity: number; price: number; active_price?: boolean }>; + prices?: Array<{ + quantity: number; + price: number; + active_price?: boolean; + promo_price?: number | null; + promo_percent?: number; + }>; hasVideo?: boolean; videoUrl?: string; categoryColor?: string; @@ -63,6 +69,11 @@ function ProductCard({ const isOutOfStock = stock === 0; const isComingSoon = coming_soon === true; const normalizedCategory = (category || "autre").toLowerCase().trim(); + const firstPromoPrice = + prices?.[0]?.promo_price != null && + prices[0].promo_price < prices[0].price + ? prices[0].promo_price + : null; const handleDetailsClick = (e: React.MouseEvent) => { e.stopPropagation(); @@ -171,9 +182,20 @@ function ProductCard({

{name}

- {price > 0 - ? `${price.toFixed(2)} €` - : "Prix non disponible"} + {price > 0 ? ( + firstPromoPrice !== null ? ( + <> + + {price.toFixed(2)} € + {" "} + {firstPromoPrice.toFixed(2)} € + + ) : ( + `${price.toFixed(2)} €` + ) + ) : ( + "Prix non disponible" + )}

@@ -224,9 +246,11 @@ function ProductCard({ key={priceOption.quantity} value={priceOption.quantity} > - {priceOption.quantity} - {unit} - {priceOption.price.toFixed(2)}{" "} - € + {priceOption.promo_price != null && + priceOption.promo_price < + priceOption.price + ? `${priceOption.quantity}${unit} - ${priceOption.promo_price.toFixed(2)} € (au lieu de ${priceOption.price.toFixed(2)} €, -${priceOption.promo_percent}%)` + : `${priceOption.quantity}${unit} - ${priceOption.price.toFixed(2)} €`} ))} diff --git a/mobile/src/components/ProductCard.tsx b/mobile/src/components/ProductCard.tsx index 5e150d0a..e6c6050f 100644 --- a/mobile/src/components/ProductCard.tsx +++ b/mobile/src/components/ProductCard.tsx @@ -47,6 +47,8 @@ interface ProductCardProps { quantity: number; price: number; active_price?: boolean; + promo_price?: number | null; + promo_percent?: number; }>; media?: Array<{ url: string; type: string }>; }; @@ -67,6 +69,11 @@ export default function ProductCard({ const activePrices = product.prices?.filter((p) => p.active_price !== false) ?? []; const firstPrice = activePrices[0]?.price ?? null; + const firstPromoPrice = + activePrices[0]?.promo_price != null && + activePrices[0].promo_price < activePrices[0].price + ? activePrices[0].promo_price + : null; const imageMedia = product.media?.find((m) => m.type === "image"); const videoMedia = product.media?.find((m) => m.type === "video"); @@ -198,11 +205,33 @@ export default function ProductCard({ > {product.name} - - {firstPrice !== null - ? `${firstPrice.toFixed(2)} €` - : "Prix non disponible"} - + {firstPrice !== null ? ( + firstPromoPrice !== null ? ( + + + {firstPrice.toFixed(2)} € + + + {firstPromoPrice.toFixed(2)} € + + + ) : ( + + {firstPrice.toFixed(2)} € + + ) + ) : ( + + Prix non disponible + + )}
- - {p.price.toFixed(2)} € - + {p.promo_price != null && + p.promo_price < p.price ? ( + + + {p.price.toFixed(2)} € + + + {p.promo_price.toFixed(2)} € + {p.promo_percent + ? ` (-${p.promo_percent}%)` + : ""} + + + ) : ( + + {p.price.toFixed(2)} € + + )}