chore: build
This commit is contained in:
@@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user