chore: build
Frontend Admin - EAS Build / build (push) Successful in 1h21m50s
Frontend Client - EAS Build / build (push) Successful in 1h17m38s

This commit is contained in:
Xor290
2026-08-30 15:23:59 +02:00
parent a35031b2a1
commit 6fca16a58c
4 changed files with 146 additions and 61 deletions
+7 -2
View File
@@ -1107,12 +1107,17 @@ export interface PointsTier {
points: number; points: number;
} }
export interface RewardProductQuantity {
product_id: number;
quantity: number; // quantité individuelle de ce produit (palier de prix catalogue, ex: 1g)
}
export interface RewardCategoryConfig { export interface RewardCategoryConfig {
category: string; category: string;
type: "free_product" | "half_price_product"; type: "free_product" | "half_price_product";
all_products: boolean; all_products: boolean;
product_ids: number[]; quantity: number; // quantité uniforme si all_products = true
quantity: number; // quantité offerte / à -50% (palier de prix catalogue, ex: 1g) products: RewardProductQuantity[]; // produits + quantité individuelle si all_products = false
} }
export interface PointsReward { export interface PointsReward {
@@ -716,39 +716,37 @@ function CategoryProductPicker({
const catProducts = products.filter((p) => p.category === catConfig.category); const catProducts = products.filter((p) => p.category === catConfig.category);
const toggleProduct = (id: number) => { const toggleProduct = (id: number) => {
const ids = catConfig.product_ids.includes(id) const exists = catConfig.products.some((pq) => pq.product_id === id);
? catConfig.product_ids.filter((x) => x !== id) if (exists) {
: [...catConfig.product_ids, id]; onChange({ ...catConfig, products: catConfig.products.filter((pq) => pq.product_id !== id), all_products: false });
onChange({ ...catConfig, product_ids: ids, 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 ( return (
<View style={{ marginTop: spacing.s, paddingLeft: spacing.m, gap: spacing.s }}> <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 */} {/* Toggle tous / sélection */}
<View style={{ flexDirection: "row", gap: spacing.s }}> <View style={{ flexDirection: "row", gap: spacing.s }}>
<TouchableOpacity <TouchableOpacity
onPress={() => onChange({ ...catConfig, all_products: true, product_ids: [] })} onPress={() => onChange({ ...catConfig, all_products: true, products: [] })}
style={{ style={{
flexDirection: "row", alignItems: "center", gap: spacing.xs, flexDirection: "row", alignItems: "center", gap: spacing.xs,
paddingHorizontal: spacing.m, paddingVertical: spacing.xs, paddingHorizontal: spacing.m, paddingVertical: spacing.xs,
@@ -779,15 +777,39 @@ function CategoryProductPicker({
</TouchableOpacity> </TouchableOpacity>
</View> </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 && ( {!catConfig.all_products && (
<View style={{ flexDirection: "row", flexWrap: "wrap", gap: spacing.xs }}> <View style={{ gap: spacing.xs }}>
{catProducts.length === 0 ? ( {catProducts.length === 0 ? (
<Text style={{ fontSize: 12, color: colors.textMuted, fontStyle: "italic" }}> <Text style={{ fontSize: 12, color: colors.textMuted, fontStyle: "italic" }}>
Aucun produit dans cette catégorie Aucun produit dans cette catégorie
</Text> </Text>
) : catProducts.map((p) => { ) : (
const sel = catConfig.product_ids.includes(p.id); <View style={{ flexDirection: "row", flexWrap: "wrap", gap: spacing.xs }}>
{catProducts.map((p) => {
const sel = catConfig.products.some((pq) => pq.product_id === p.id);
return ( return (
<TouchableOpacity <TouchableOpacity
key={p.id} key={p.id}
@@ -809,6 +831,51 @@ function CategoryProductPicker({
})} })}
</View> </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> </View>
); );
} }
@@ -844,7 +911,7 @@ function CentralRewardSection({
// propre sélection de produits — d'où la recherche par (catégorie, type). // propre sélection de produits — d'où la recherche par (catégorie, type).
const getCatConfig = (catName: string, type: RewardCategoryConfig["type"]): RewardCategoryConfig => const getCatConfig = (catName: string, type: RewardCategoryConfig["type"]): RewardCategoryConfig =>
r.category_configs.find((c) => c.category === catName && c.type === type) ?? 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"]) => const isTypeEnabled = (catName: string, type: RewardCategoryConfig["type"]) =>
r.category_configs.some((c) => c.category === catName && c.type === type); r.category_configs.some((c) => c.category === catName && c.type === type);
@@ -856,7 +923,7 @@ function CentralRewardSection({
if (isTypeEnabled(catName, type)) { if (isTypeEnabled(catName, type)) {
update({ category_configs: r.category_configs.filter((c) => !(c.category === catName && c.type === type)) }); update({ category_configs: r.category_configs.filter((c) => !(c.category === catName && c.type === type)) });
} else { } 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) => ( {r.category_configs.map((cfg, idx) => (
<Text key={`${cfg.category}-${cfg.type}-${idx}`} style={{ fontSize: 12, color: colors.textSecondary }}> <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> </Text>
))} ))}
</View> </View>
@@ -1180,7 +1249,13 @@ export default function SettingsScreen() {
category_routes: s.delivery_mode?.category_routes ?? [], category_routes: s.delivery_mode?.category_routes ?? [],
}, },
points_reward: s.points_reward 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, : null,
}); });
} }
+7 -2
View File
@@ -961,12 +961,17 @@ export const toggle2FA = async (
// 🏆 POINTS — RÉCOMPENSES // 🏆 POINTS — RÉCOMPENSES
// ============================================ // ============================================
export type RewardConfigProduct = {
product_id: number;
product_name: string;
quantity: number;
};
export type RewardCategoryConfig = { export type RewardCategoryConfig = {
category: string; category: string;
type: "free_product" | "half_price_product"; type: "free_product" | "half_price_product";
all_products: boolean; all_products: boolean;
product_ids: number[]; products: RewardConfigProduct[];
product_names: string[];
quantity: number; quantity: number;
}; };
@@ -830,14 +830,14 @@ export default function OrderHistoryScreen() {
</View>, </View>,
] ]
: ( : (
cfg.product_names ?? cfg.products ??
[] []
).map( ).map(
( (
name, p,
) => ( ) => (
<View <View
key={`${cfg.category}-${name}`} key={`${cfg.category}-${p.product_id}`}
style={ style={
styles.rewardAmountBadge styles.rewardAmountBadge
} }
@@ -848,7 +848,7 @@ export default function OrderHistoryScreen() {
} }
> >
{ {
name p.product_name
} }
</Text> </Text>
</View> </View>