chore: build
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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 (
|
||||
<View style={{ marginTop: spacing.s, paddingLeft: spacing.m, gap: spacing.s }}>
|
||||
{/* 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%"). */}
|
||||
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.xs }}>
|
||||
<Text style={{ fontSize: 12, color: colors.textMuted }}>Quantité :</Text>
|
||||
<TextInput
|
||||
style={[s.thresholdInput, { width: 56, fontSize: 13 }]}
|
||||
keyboardType="decimal-pad"
|
||||
value={catConfig.quantity > 0 ? String(catConfig.quantity) : ""}
|
||||
onChangeText={(v) => {
|
||||
const n = parseFloat(v);
|
||||
onChange({ ...catConfig, quantity: isNaN(n) ? 0 : n });
|
||||
}}
|
||||
placeholder="1"
|
||||
placeholderTextColor={colors.textMuted}
|
||||
/>
|
||||
<Text style={{ fontSize: 11, color: colors.textMuted, fontStyle: "italic" }}>
|
||||
doit correspondre à un palier de prix existant
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Toggle tous / sélection */}
|
||||
<View style={{ flexDirection: "row", gap: spacing.s }}>
|
||||
<TouchableOpacity
|
||||
onPress={() => 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({
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Liste des produits si mode sélection */}
|
||||
{/* Mode "Tous" : une quantité uniforme pour tous les produits de la catégorie */}
|
||||
{catConfig.all_products && (
|
||||
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.xs }}>
|
||||
<Text style={{ fontSize: 12, color: colors.textMuted }}>Quantité :</Text>
|
||||
<TextInput
|
||||
style={[s.thresholdInput, { width: 56, fontSize: 13 }]}
|
||||
keyboardType="decimal-pad"
|
||||
value={catConfig.quantity > 0 ? String(catConfig.quantity) : ""}
|
||||
onChangeText={(v) => {
|
||||
const n = parseFloat(v);
|
||||
onChange({ ...catConfig, quantity: isNaN(n) ? 0 : n });
|
||||
}}
|
||||
placeholder="1"
|
||||
placeholderTextColor={colors.textMuted}
|
||||
/>
|
||||
<Text style={{ fontSize: 11, color: colors.textMuted, fontStyle: "italic" }}>
|
||||
doit correspondre à un palier de prix existant
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Mode "Sélection" : chaque produit choisi a sa propre quantité
|
||||
(ex: produit A à 2g offerts, produit B à 1g offert) */}
|
||||
{!catConfig.all_products && (
|
||||
<View style={{ flexDirection: "row", flexWrap: "wrap", gap: spacing.xs }}>
|
||||
<View style={{ gap: spacing.xs }}>
|
||||
{catProducts.length === 0 ? (
|
||||
<Text style={{ fontSize: 12, color: colors.textMuted, fontStyle: "italic" }}>
|
||||
Aucun produit dans cette catégorie
|
||||
</Text>
|
||||
) : catProducts.map((p) => {
|
||||
const sel = catConfig.product_ids.includes(p.id);
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={p.id}
|
||||
onPress={() => 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 && <Ionicons name="checkmark" size={11} color={REWARD_ACCENT} />}
|
||||
<Text style={{ fontSize: 12, fontWeight: sel ? "700" : "400", color: sel ? REWARD_ACCENT : colors.textMuted }}>
|
||||
{p.name}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
) : (
|
||||
<View style={{ flexDirection: "row", flexWrap: "wrap", gap: spacing.xs }}>
|
||||
{catProducts.map((p) => {
|
||||
const sel = catConfig.products.some((pq) => pq.product_id === p.id);
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={p.id}
|
||||
onPress={() => 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 && <Ionicons name="checkmark" size={11} color={REWARD_ACCENT} />}
|
||||
<Text style={{ fontSize: 12, fontWeight: sel ? "700" : "400", color: sel ? REWARD_ACCENT : colors.textMuted }}>
|
||||
{p.name}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{catConfig.products.length > 0 && (
|
||||
<View style={{ gap: spacing.s, marginTop: spacing.xs }}>
|
||||
{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 (
|
||||
<View key={pq.product_id} style={{ gap: 4 }}>
|
||||
<Text style={{ fontSize: 12, color: colors.textMuted }} numberOfLines={1}>
|
||||
{prod?.name ?? `Produit #${pq.product_id}`}
|
||||
</Text>
|
||||
<View style={{ flexDirection: "row", flexWrap: "wrap", gap: 4 }}>
|
||||
{tiers.length === 0 ? (
|
||||
<Text style={{ fontSize: 11, color: colors.textMuted, fontStyle: "italic" }}>
|
||||
Aucun palier de prix actif pour ce produit
|
||||
</Text>
|
||||
) : tiers.map((tier) => {
|
||||
const isSel = pq.quantity === tier.quantity;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={tier.quantity}
|
||||
onPress={() => 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 && <Ionicons name="checkmark" size={10} color={REWARD_ACCENT} />}
|
||||
<Text style={{ fontSize: 11, fontWeight: isSel ? "700" : "400", color: isSel ? REWARD_ACCENT : colors.textMuted }}>
|
||||
{tier.quantity}{prod?.unit ?? ""} · {tier.price}€
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
@@ -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) => (
|
||||
<Text key={`${cfg.category}-${cfg.type}-${idx}`} style={{ fontSize: 12, color: colors.textSecondary }}>
|
||||
• {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(", ")}`}
|
||||
</Text>
|
||||
))}
|
||||
</View>
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user