From 6fca16a58c91bca5222d3b40de1fda523ea9e458 Mon Sep 17 00:00:00 2001 From: Xor290 Date: Sun, 30 Aug 2026 15:23:59 +0200 Subject: [PATCH] chore: build --- frontend-admin/src/api/api_admin.ts | 9 +- .../src/screens/admin/SettingsScreen.tsx | 181 +++++++++++++----- mobile/src/api/api.ts | 9 +- .../src/screens/client/OrderHistoryScreen.tsx | 8 +- 4 files changed, 146 insertions(+), 61 deletions(-) diff --git a/frontend-admin/src/api/api_admin.ts b/frontend-admin/src/api/api_admin.ts index 89e8e2da..615905cf 100644 --- a/frontend-admin/src/api/api_admin.ts +++ b/frontend-admin/src/api/api_admin.ts @@ -1107,12 +1107,17 @@ export interface PointsTier { points: number; } +export interface RewardProductQuantity { + product_id: number; + quantity: number; // quantité individuelle de ce produit (palier de prix catalogue, ex: 1g) +} + export interface RewardCategoryConfig { category: string; type: "free_product" | "half_price_product"; all_products: boolean; - product_ids: number[]; - quantity: number; // quantité offerte / à -50% (palier de prix catalogue, ex: 1g) + quantity: number; // quantité uniforme si all_products = true + products: RewardProductQuantity[]; // produits + quantité individuelle si all_products = false } export interface PointsReward { diff --git a/frontend-admin/src/screens/admin/SettingsScreen.tsx b/frontend-admin/src/screens/admin/SettingsScreen.tsx index c1884873..278fdfbf 100644 --- a/frontend-admin/src/screens/admin/SettingsScreen.tsx +++ b/frontend-admin/src/screens/admin/SettingsScreen.tsx @@ -716,39 +716,37 @@ function CategoryProductPicker({ const catProducts = products.filter((p) => p.category === catConfig.category); const toggleProduct = (id: number) => { - const ids = catConfig.product_ids.includes(id) - ? catConfig.product_ids.filter((x) => x !== id) - : [...catConfig.product_ids, id]; - onChange({ ...catConfig, product_ids: ids, all_products: false }); + const exists = catConfig.products.some((pq) => pq.product_id === id); + if (exists) { + onChange({ ...catConfig, products: catConfig.products.filter((pq) => pq.product_id !== id), all_products: false }); + return; + } + // Présélectionne le premier palier de prix actif du produit, plutôt + // qu'une valeur arbitraire qui pourrait ne correspondre à aucun palier réel. + const prod = catProducts.find((p) => p.id === id); + const firstTier = (prod?.prices ?? []).find((pr) => pr.active_price !== false); + onChange({ + ...catConfig, + products: [...catConfig.products, { product_id: id, quantity: firstTier?.quantity ?? 0 }], + all_products: false, + }); + }; + + const updateProductQuantity = (id: number, quantity: number) => { + onChange({ + ...catConfig, + products: catConfig.products.map((pq) => + pq.product_id === id ? { ...pq, quantity } : pq + ), + }); }; return ( - {/* Quantité offerte / à -50% — correspond à un palier de prix du - catalogue (ex: si le produit a un palier quantity=1 à 30€, - indiquer 1 ici donne "30€ offert" ou "15€ à -50%"). */} - - Quantité : - 0 ? String(catConfig.quantity) : ""} - onChangeText={(v) => { - const n = parseFloat(v); - onChange({ ...catConfig, quantity: isNaN(n) ? 0 : n }); - }} - placeholder="1" - placeholderTextColor={colors.textMuted} - /> - - doit correspondre à un palier de prix existant - - - {/* Toggle tous / sélection */} onChange({ ...catConfig, all_products: true, product_ids: [] })} + onPress={() => onChange({ ...catConfig, all_products: true, products: [] })} style={{ flexDirection: "row", alignItems: "center", gap: spacing.xs, paddingHorizontal: spacing.m, paddingVertical: spacing.xs, @@ -779,34 +777,103 @@ function CategoryProductPicker({ - {/* Liste des produits si mode sélection */} + {/* Mode "Tous" : une quantité uniforme pour tous les produits de la catégorie */} + {catConfig.all_products && ( + + Quantité : + 0 ? String(catConfig.quantity) : ""} + onChangeText={(v) => { + const n = parseFloat(v); + onChange({ ...catConfig, quantity: isNaN(n) ? 0 : n }); + }} + placeholder="1" + placeholderTextColor={colors.textMuted} + /> + + doit correspondre à un palier de prix existant + + + )} + + {/* Mode "Sélection" : chaque produit choisi a sa propre quantité + (ex: produit A à 2g offerts, produit B à 1g offert) */} {!catConfig.all_products && ( - + {catProducts.length === 0 ? ( Aucun produit dans cette catégorie - ) : catProducts.map((p) => { - const sel = catConfig.product_ids.includes(p.id); - return ( - toggleProduct(p.id)} - style={{ - paddingHorizontal: spacing.s, paddingVertical: 4, - borderRadius: borderRadius.sm, borderWidth: 1.5, - borderColor: sel ? REWARD_ACCENT : colors.border, - backgroundColor: sel ? REWARD_ACCENT + "22" : "transparent", - flexDirection: "row", alignItems: "center", gap: 4, - }} - > - {sel && } - - {p.name} - - - ); - })} + ) : ( + + {catProducts.map((p) => { + const sel = catConfig.products.some((pq) => pq.product_id === p.id); + return ( + toggleProduct(p.id)} + style={{ + paddingHorizontal: spacing.s, paddingVertical: 4, + borderRadius: borderRadius.sm, borderWidth: 1.5, + borderColor: sel ? REWARD_ACCENT : colors.border, + backgroundColor: sel ? REWARD_ACCENT + "22" : "transparent", + flexDirection: "row", alignItems: "center", gap: 4, + }} + > + {sel && } + + {p.name} + + + ); + })} + + )} + + {catConfig.products.length > 0 && ( + + {catConfig.products.map((pq) => { + const prod = catProducts.find((p) => p.id === pq.product_id); + const tiers = (prod?.prices ?? []).filter((pr) => pr.active_price !== false); + return ( + + + {prod?.name ?? `Produit #${pq.product_id}`} + + + {tiers.length === 0 ? ( + + Aucun palier de prix actif pour ce produit + + ) : tiers.map((tier) => { + const isSel = pq.quantity === tier.quantity; + return ( + updateProductQuantity(pq.product_id, tier.quantity)} + style={{ + paddingHorizontal: spacing.s, paddingVertical: 3, + borderRadius: borderRadius.sm, borderWidth: 1.5, + borderColor: isSel ? REWARD_ACCENT : colors.border, + backgroundColor: isSel ? REWARD_ACCENT + "22" : "transparent", + flexDirection: "row", alignItems: "center", gap: 4, + }} + > + {isSel && } + + {tier.quantity}{prod?.unit ?? ""} · {tier.price}€ + + + ); + })} + + + ); + })} + + )} )} @@ -844,7 +911,7 @@ function CentralRewardSection({ // propre sélection de produits — d'où la recherche par (catégorie, type). const getCatConfig = (catName: string, type: RewardCategoryConfig["type"]): RewardCategoryConfig => r.category_configs.find((c) => c.category === catName && c.type === type) ?? - { category: catName, type, all_products: true, product_ids: [], quantity: 1 }; + { category: catName, type, all_products: true, products: [], quantity: 1 }; const isTypeEnabled = (catName: string, type: RewardCategoryConfig["type"]) => r.category_configs.some((c) => c.category === catName && c.type === type); @@ -856,7 +923,7 @@ function CentralRewardSection({ if (isTypeEnabled(catName, type)) { update({ category_configs: r.category_configs.filter((c) => !(c.category === catName && c.type === type)) }); } else { - update({ category_configs: [...r.category_configs, { category: catName, type, all_products: true, product_ids: [], quantity: 1 }] }); + update({ category_configs: [...r.category_configs, { category: catName, type, all_products: true, products: [], quantity: 1 }] }); } }; @@ -1043,7 +1110,9 @@ function CentralRewardSection({ )} {r.category_configs.map((cfg, idx) => ( - • {REWARD_TYPES.find((x) => x.value === (cfg.type || "free_product"))?.label} : {cfg.category} — {cfg.all_products ? "tous les produits" : `${cfg.product_ids.length} produit(s)`} · qté {cfg.quantity > 0 ? cfg.quantity : 1} + • {REWARD_TYPES.find((x) => x.value === (cfg.type || "free_product"))?.label} : {cfg.category} — {cfg.all_products + ? `tous les produits · qté ${cfg.quantity > 0 ? cfg.quantity : 1}` + : `${cfg.products.length} produit(s) : ${cfg.products.map((pq) => `#${pq.product_id}×${pq.quantity}`).join(", ")}`} ))} @@ -1180,7 +1249,13 @@ export default function SettingsScreen() { category_routes: s.delivery_mode?.category_routes ?? [], }, points_reward: s.points_reward - ? { ...s.points_reward, category_configs: s.points_reward.category_configs ?? [] } + ? { + ...s.points_reward, + category_configs: (s.points_reward.category_configs ?? []).map((cfg) => ({ + ...cfg, + products: cfg.products ?? [], + })), + } : null, }); } diff --git a/mobile/src/api/api.ts b/mobile/src/api/api.ts index 6380eac1..644e33b3 100644 --- a/mobile/src/api/api.ts +++ b/mobile/src/api/api.ts @@ -961,12 +961,17 @@ export const toggle2FA = async ( // 🏆 POINTS — RÉCOMPENSES // ============================================ +export type RewardConfigProduct = { + product_id: number; + product_name: string; + quantity: number; +}; + export type RewardCategoryConfig = { category: string; type: "free_product" | "half_price_product"; all_products: boolean; - product_ids: number[]; - product_names: string[]; + products: RewardConfigProduct[]; quantity: number; }; diff --git a/mobile/src/screens/client/OrderHistoryScreen.tsx b/mobile/src/screens/client/OrderHistoryScreen.tsx index f1dc37a4..ee4a6bc0 100644 --- a/mobile/src/screens/client/OrderHistoryScreen.tsx +++ b/mobile/src/screens/client/OrderHistoryScreen.tsx @@ -830,14 +830,14 @@ export default function OrderHistoryScreen() { , ] : ( - cfg.product_names ?? + cfg.products ?? [] ).map( ( - name, + p, ) => ( { - name + p.product_name }