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;
}
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,15 +777,39 @@ 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);
) : (
<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}
@@ -809,6 +831,51 @@ function CategoryProductPicker({
})}
</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,
});
}
+7 -2
View File
@@ -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;
};
@@ -830,14 +830,14 @@ export default function OrderHistoryScreen() {
</View>,
]
: (
cfg.product_names ??
cfg.products ??
[]
).map(
(
name,
p,
) => (
<View
key={`${cfg.category}-${name}`}
key={`${cfg.category}-${p.product_id}`}
style={
styles.rewardAmountBadge
}
@@ -848,7 +848,7 @@ export default function OrderHistoryScreen() {
}
>
{
name
p.product_name
}
</Text>
</View>