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
}