chore: build
This commit is contained in:
@@ -1139,6 +1139,23 @@ export interface CategoryPromotionConfig {
|
||||
products: PromotionProductQuantity[]; // produits + quantité individuelle si all_products = false
|
||||
}
|
||||
|
||||
export interface FreeGiftTier {
|
||||
buy_quantity: number; // quantité à acheter pour déclencher l'offre
|
||||
free_quantity: number; // quantité offerte du même produit
|
||||
}
|
||||
|
||||
export interface FreeGiftProductQuantity {
|
||||
product_id: number;
|
||||
tiers: FreeGiftTier[]; // seuils propres à ce produit
|
||||
}
|
||||
|
||||
export interface CategoryFreeGiftConfig {
|
||||
category: string;
|
||||
all_products: boolean;
|
||||
tiers: FreeGiftTier[]; // seuils uniformes si all_products = true
|
||||
products: FreeGiftProductQuantity[]; // produits + seuils individuels si all_products = false
|
||||
}
|
||||
|
||||
export interface PointsPool {
|
||||
key: string;
|
||||
name: string;
|
||||
@@ -1240,6 +1257,8 @@ export interface AppSettings {
|
||||
points_reward?: PointsReward | null;
|
||||
promotions_enabled: boolean;
|
||||
promotions: CategoryPromotionConfig[];
|
||||
free_gifts_enabled: boolean;
|
||||
free_gifts: CategoryFreeGiftConfig[];
|
||||
referral_enabled: boolean;
|
||||
delivery_schedule: DeliverySchedule;
|
||||
postal_zones: PostalZone[];
|
||||
|
||||
@@ -17,7 +17,7 @@ import { Ionicons } from "@expo/vector-icons";
|
||||
import { spacing, fontSize, borderRadius } from "../../theme";
|
||||
import { useTheme } from "../../context/ThemeContext";
|
||||
import { getSettings, updateSettings, getCategories, getAvailableDeliveryPersons, getAllProductsAdmin, DEFAULT_DELIVERY_SCHEDULE, DEFAULT_POSTAL_ZONES } from "../../api/api_admin";
|
||||
import type { AppSettings, Category, CategoryRoute, DeliveryModeConfig, PointsTier, PointsPool, PointsReward, RewardCategoryConfig, CategoryPromotionConfig, DaySchedule, DeliverySchedule, PostalZone } from "../../api/api_admin";
|
||||
import type { AppSettings, Category, CategoryRoute, DeliveryModeConfig, PointsTier, PointsPool, PointsReward, RewardCategoryConfig, CategoryPromotionConfig, CategoryFreeGiftConfig, FreeGiftProductQuantity, FreeGiftTier, DaySchedule, DeliverySchedule, PostalZone } from "../../api/api_admin";
|
||||
import type { Product } from "../../api/types";
|
||||
import AlertModal from "../../components/ui/AlertModal";
|
||||
import { useAlert } from "../../hooks/useAlert";
|
||||
@@ -1520,6 +1520,405 @@ function PromotionsSection({
|
||||
);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// Offres "achetez X, Y offert" — quantité supplémentaire du même
|
||||
// produit livrée gratuitement dès qu'un seuil d'achat est atteint,
|
||||
// indépendant des points et des promotions (cumulable avec elles).
|
||||
// Plusieurs seuils peuvent coexister sur un même produit (ex: 10g→+1g,
|
||||
// 20g→+3g) : le seuil le plus élevé atteint par la commande est retenu.
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
const FREEGIFT_ACCENT = "#f59e0b";
|
||||
|
||||
function FreeGiftTierListEditor({
|
||||
tiers,
|
||||
onChange,
|
||||
colors,
|
||||
s,
|
||||
}: {
|
||||
tiers: FreeGiftTier[];
|
||||
onChange: (tiers: FreeGiftTier[]) => void;
|
||||
colors: any;
|
||||
s: any;
|
||||
}) {
|
||||
const updateTier = (idx: number, patch: Partial<FreeGiftTier>) => {
|
||||
onChange(tiers.map((t, i) => (i === idx ? { ...t, ...patch } : t)));
|
||||
};
|
||||
const removeTier = (idx: number) => {
|
||||
onChange(tiers.filter((_, i) => i !== idx));
|
||||
};
|
||||
const addTier = () => {
|
||||
onChange([...tiers, { buy_quantity: 0, free_quantity: 0 }]);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={{ gap: spacing.xs }}>
|
||||
{tiers.map((t, idx) => (
|
||||
<View key={idx} style={{ flexDirection: "row", alignItems: "center", gap: spacing.xs }}>
|
||||
<Text style={{ fontSize: 11, color: colors.textMuted }}>Acheté :</Text>
|
||||
<TextInput
|
||||
style={[s.thresholdInput, { width: 50, fontSize: 12 }]}
|
||||
keyboardType="decimal-pad"
|
||||
value={t.buy_quantity > 0 ? String(t.buy_quantity) : ""}
|
||||
onChangeText={(v) => {
|
||||
const n = parseFloat(v);
|
||||
updateTier(idx, { buy_quantity: isNaN(n) ? 0 : n });
|
||||
}}
|
||||
placeholder="10"
|
||||
placeholderTextColor={colors.textMuted}
|
||||
/>
|
||||
<Ionicons name="arrow-forward" size={12} color={colors.textMuted} />
|
||||
<Text style={{ fontSize: 11, color: colors.textMuted }}>Offert :</Text>
|
||||
<TextInput
|
||||
style={[s.thresholdInput, { width: 50, fontSize: 12 }]}
|
||||
keyboardType="decimal-pad"
|
||||
value={t.free_quantity > 0 ? String(t.free_quantity) : ""}
|
||||
onChangeText={(v) => {
|
||||
const n = parseFloat(v);
|
||||
updateTier(idx, { free_quantity: isNaN(n) ? 0 : n });
|
||||
}}
|
||||
placeholder="1"
|
||||
placeholderTextColor={colors.textMuted}
|
||||
/>
|
||||
<TouchableOpacity onPress={() => removeTier(idx)} hitSlop={8}>
|
||||
<Ionicons name="trash-outline" size={15} color={colors.danger ?? "#ef4444"} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
))}
|
||||
<TouchableOpacity
|
||||
onPress={addTier}
|
||||
style={{ flexDirection: "row", alignItems: "center", gap: 4, alignSelf: "flex-start", marginTop: 2 }}
|
||||
>
|
||||
<Ionicons name="add-circle-outline" size={14} color={FREEGIFT_ACCENT} />
|
||||
<Text style={{ fontSize: 12, color: FREEGIFT_ACCENT, fontWeight: "600" }}>Ajouter un seuil</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function FreeGiftProductPicker({
|
||||
catConfig,
|
||||
products,
|
||||
onChange,
|
||||
colors,
|
||||
s,
|
||||
}: {
|
||||
catConfig: CategoryFreeGiftConfig;
|
||||
products: Product[];
|
||||
onChange: (cfg: CategoryFreeGiftConfig) => void;
|
||||
colors: any;
|
||||
s: any;
|
||||
}) {
|
||||
const catProducts = products.filter((p) => p.category === catConfig.category);
|
||||
|
||||
const toggleProduct = (id: number) => {
|
||||
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;
|
||||
}
|
||||
onChange({
|
||||
...catConfig,
|
||||
products: [...catConfig.products, { product_id: id, tiers: [{ buy_quantity: 0, free_quantity: 0 }] }],
|
||||
all_products: false,
|
||||
});
|
||||
};
|
||||
|
||||
const updateProductTiers = (id: number, tiers: FreeGiftTier[]) => {
|
||||
onChange({
|
||||
...catConfig,
|
||||
products: catConfig.products.map((pq) => (pq.product_id === id ? { ...pq, tiers } : pq)),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={{ marginTop: spacing.s, paddingLeft: spacing.m, gap: spacing.s }}>
|
||||
{/* Toggle tous / sélection */}
|
||||
<View style={{ flexDirection: "row", gap: spacing.s }}>
|
||||
<TouchableOpacity
|
||||
onPress={() => onChange({ ...catConfig, all_products: true, products: [] })}
|
||||
style={{
|
||||
flexDirection: "row", alignItems: "center", gap: spacing.xs,
|
||||
paddingHorizontal: spacing.m, paddingVertical: spacing.xs,
|
||||
borderRadius: borderRadius.full, borderWidth: 1.5,
|
||||
borderColor: catConfig.all_products ? FREEGIFT_ACCENT : colors.border,
|
||||
backgroundColor: catConfig.all_products ? FREEGIFT_ACCENT + "22" : "transparent",
|
||||
}}
|
||||
>
|
||||
<Ionicons name="checkmark-done-outline" size={13} color={catConfig.all_products ? FREEGIFT_ACCENT : colors.textMuted} />
|
||||
<Text style={{ fontSize: 12, fontWeight: catConfig.all_products ? "700" : "400", color: catConfig.all_products ? FREEGIFT_ACCENT : colors.textMuted }}>
|
||||
Tous ({catProducts.length})
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={() => onChange({ ...catConfig, all_products: false })}
|
||||
style={{
|
||||
flexDirection: "row", alignItems: "center", gap: spacing.xs,
|
||||
paddingHorizontal: spacing.m, paddingVertical: spacing.xs,
|
||||
borderRadius: borderRadius.full, borderWidth: 1.5,
|
||||
borderColor: !catConfig.all_products ? FREEGIFT_ACCENT : colors.border,
|
||||
backgroundColor: !catConfig.all_products ? FREEGIFT_ACCENT + "22" : "transparent",
|
||||
}}
|
||||
>
|
||||
<Ionicons name="list-outline" size={13} color={!catConfig.all_products ? FREEGIFT_ACCENT : colors.textMuted} />
|
||||
<Text style={{ fontSize: 12, fontWeight: !catConfig.all_products ? "700" : "400", color: !catConfig.all_products ? FREEGIFT_ACCENT : colors.textMuted }}>
|
||||
Sélection
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Mode "Tous" : seuils uniformes pour tous les produits de la catégorie */}
|
||||
{catConfig.all_products && (
|
||||
<View>
|
||||
<Text style={{ fontSize: 11, color: colors.textMuted, fontStyle: "italic", marginBottom: 4 }}>
|
||||
Les quantités achetées doivent correspondre à des paliers de prix existants
|
||||
</Text>
|
||||
<FreeGiftTierListEditor
|
||||
tiers={catConfig.tiers}
|
||||
onChange={(tiers) => onChange({ ...catConfig, tiers })}
|
||||
colors={colors}
|
||||
s={s}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Mode "Sélection" : chaque produit choisi a ses propres seuils */}
|
||||
{!catConfig.all_products && (
|
||||
<View style={{ gap: spacing.xs }}>
|
||||
{catProducts.length === 0 ? (
|
||||
<Text style={{ fontSize: 12, color: colors.textMuted, fontStyle: "italic" }}>
|
||||
Aucun produit dans cette catégorie
|
||||
</Text>
|
||||
) : (
|
||||
<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 ? FREEGIFT_ACCENT : colors.border,
|
||||
backgroundColor: sel ? FREEGIFT_ACCENT + "22" : "transparent",
|
||||
flexDirection: "row", alignItems: "center", gap: 4,
|
||||
}}
|
||||
>
|
||||
{sel && <Ionicons name="checkmark" size={11} color={FREEGIFT_ACCENT} />}
|
||||
<Text style={{ fontSize: 12, fontWeight: sel ? "700" : "400", color: sel ? FREEGIFT_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);
|
||||
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>
|
||||
<FreeGiftTierListEditor
|
||||
tiers={pq.tiers}
|
||||
onChange={(tiers) => updateProductTiers(pq.product_id, tiers)}
|
||||
colors={colors}
|
||||
s={s}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function FreeGiftsSection({
|
||||
enabled,
|
||||
freeGifts,
|
||||
allCategories,
|
||||
productsByCategory,
|
||||
onToggle,
|
||||
onChangeFreeGifts,
|
||||
colors,
|
||||
s,
|
||||
}: {
|
||||
enabled: boolean;
|
||||
freeGifts: CategoryFreeGiftConfig[];
|
||||
allCategories: Category[];
|
||||
productsByCategory: Record<string, Product[]>;
|
||||
onToggle: (v: boolean) => void;
|
||||
onChangeFreeGifts: (freeGifts: CategoryFreeGiftConfig[]) => void;
|
||||
colors: any;
|
||||
s: any;
|
||||
}) {
|
||||
const getCatConfig = (catName: string): CategoryFreeGiftConfig =>
|
||||
freeGifts.find((g) => g.category === catName) ??
|
||||
{ category: catName, all_products: true, tiers: [], products: [] };
|
||||
|
||||
const isCatSelected = (catName: string) => freeGifts.some((g) => g.category === catName);
|
||||
|
||||
const toggleCategory = (catName: string) => {
|
||||
if (isCatSelected(catName)) {
|
||||
onChangeFreeGifts(freeGifts.filter((g) => g.category !== catName));
|
||||
} else {
|
||||
onChangeFreeGifts([...freeGifts, { category: catName, all_products: true, tiers: [], products: [] }]);
|
||||
}
|
||||
};
|
||||
|
||||
const updateCatConfig = (cfg: CategoryFreeGiftConfig) => {
|
||||
onChangeFreeGifts(freeGifts.map((g) => (g.category === cfg.category ? cfg : g)));
|
||||
};
|
||||
|
||||
const [expandedCats, setExpandedCats] = useState<Set<string>>(new Set());
|
||||
const toggleExpanded = (catName: string) => {
|
||||
setExpandedCats((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(catName)) next.delete(catName); else next.add(catName);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const countTiers = (cfg: CategoryFreeGiftConfig) =>
|
||||
cfg.all_products ? cfg.tiers.length : cfg.products.reduce((sum, pq) => sum + pq.tiers.length, 0);
|
||||
|
||||
const badge = (
|
||||
<View style={{
|
||||
paddingHorizontal: spacing.s, paddingVertical: 2, borderRadius: 10,
|
||||
backgroundColor: enabled ? FREEGIFT_ACCENT + "25" : colors.border + "40",
|
||||
borderWidth: 1, borderColor: enabled ? FREEGIFT_ACCENT : colors.border,
|
||||
}}>
|
||||
<Text style={{ fontSize: 10, fontWeight: "700", color: enabled ? FREEGIFT_ACCENT : colors.textMuted }}>
|
||||
{enabled ? "Activées" : "Désactivées"}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<AccordionSection title="Offres quantité offerte" badge={badge} colors={colors} s={s}>
|
||||
<View style={[s.row, s.rowFirst]}>
|
||||
<View style={s.rowLeft}>
|
||||
<Text style={s.rowLabel}>Offres activées</Text>
|
||||
<Text style={s.rowDesc}>
|
||||
Quantité supplémentaire du même produit livrée gratuitement dès qu'un seuil d'achat est atteint (ex: 10g achetés → 1g offert) — indépendant des points et des promotions, cumulable avec elles.
|
||||
</Text>
|
||||
</View>
|
||||
<Switch
|
||||
value={enabled}
|
||||
onValueChange={onToggle}
|
||||
trackColor={{ false: colors.border, true: FREEGIFT_ACCENT }}
|
||||
thumbColor="#fff"
|
||||
/>
|
||||
</View>
|
||||
|
||||
{enabled && (
|
||||
<View style={{ borderTopWidth: 1, borderTopColor: colors.border, paddingHorizontal: spacing.l, paddingTop: spacing.l, paddingBottom: spacing.l, gap: spacing.l }}>
|
||||
<View>
|
||||
<Text style={[s.rowLabel, { marginBottom: spacing.xs }]}>Catégories concernées</Text>
|
||||
<Text style={[s.rowDesc, { marginBottom: spacing.m }]}>
|
||||
Sélectionnez une catégorie, puis tous les produits ou une sélection, avec un ou plusieurs seuils achat/offert par produit.
|
||||
</Text>
|
||||
{allCategories.length === 0 ? (
|
||||
<Text style={[s.hint, { paddingHorizontal: 0 }]}>Aucune catégorie disponible</Text>
|
||||
) : (
|
||||
<View style={{ gap: spacing.m }}>
|
||||
{allCategories.map((cat) => {
|
||||
const selected = isCatSelected(cat.name);
|
||||
const expanded = expandedCats.has(cat.name);
|
||||
const catColor = cat.color || FREEGIFT_ACCENT;
|
||||
const cfg = getCatConfig(cat.name);
|
||||
return (
|
||||
<View key={cat.name}>
|
||||
<TouchableOpacity
|
||||
onPress={() => toggleExpanded(cat.name)}
|
||||
style={{
|
||||
flexDirection: "row", alignItems: "center", gap: spacing.xs,
|
||||
alignSelf: "flex-start",
|
||||
paddingHorizontal: spacing.m, paddingVertical: spacing.s,
|
||||
borderRadius: borderRadius.full, borderWidth: 1.5,
|
||||
borderColor: selected ? catColor : colors.border,
|
||||
backgroundColor: selected ? catColor + "22" : "transparent",
|
||||
}}
|
||||
>
|
||||
<View style={{ width: 8, height: 8, borderRadius: 4, backgroundColor: catColor }} />
|
||||
<Text style={{ fontSize: 13, fontWeight: selected ? "700" : "400", color: selected ? catColor : colors.textMuted }}>
|
||||
{cat.name}{selected && countTiers(cfg) > 0 ? ` · ${countTiers(cfg)} seuil(s)` : ""}
|
||||
</Text>
|
||||
<Ionicons
|
||||
name={expanded ? "chevron-down" : "chevron-forward"}
|
||||
size={12}
|
||||
color={selected ? catColor : colors.textMuted}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
{expanded && (
|
||||
<View style={{ marginTop: spacing.s, paddingLeft: spacing.m, gap: spacing.s }}>
|
||||
<TouchableOpacity
|
||||
onPress={() => toggleCategory(cat.name)}
|
||||
style={{
|
||||
flexDirection: "row", alignItems: "center", gap: 4,
|
||||
alignSelf: "flex-start",
|
||||
paddingHorizontal: spacing.s, paddingVertical: 4,
|
||||
borderRadius: borderRadius.sm, borderWidth: 1.5,
|
||||
borderColor: selected ? FREEGIFT_ACCENT : colors.border,
|
||||
backgroundColor: selected ? FREEGIFT_ACCENT + "22" : "transparent",
|
||||
}}
|
||||
>
|
||||
<Ionicons
|
||||
name={selected ? "checkbox" : "square-outline"}
|
||||
size={14}
|
||||
color={selected ? FREEGIFT_ACCENT : colors.textMuted}
|
||||
/>
|
||||
<Ionicons name="gift-outline" size={12} color={selected ? FREEGIFT_ACCENT : colors.textMuted} />
|
||||
<Text style={{ fontSize: 12, fontWeight: selected ? "700" : "400", color: selected ? FREEGIFT_ACCENT : colors.textMuted }}>
|
||||
Offre active sur cette catégorie
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{selected && (
|
||||
<FreeGiftProductPicker
|
||||
catConfig={cfg}
|
||||
products={productsByCategory[cat.name] ?? []}
|
||||
onChange={updateCatConfig}
|
||||
colors={colors}
|
||||
s={s}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Récapitulatif */}
|
||||
{freeGifts.length > 0 && (
|
||||
<View style={{ backgroundColor: FREEGIFT_ACCENT + "12", borderRadius: borderRadius.sm, borderLeftWidth: 3, borderLeftColor: FREEGIFT_ACCENT, padding: spacing.m, gap: 4 }}>
|
||||
<Text style={{ fontSize: 13, fontWeight: "700", color: FREEGIFT_ACCENT }}>Récapitulatif</Text>
|
||||
{freeGifts.map((cfg, idx) => (
|
||||
<Text key={`${cfg.category}-${idx}`} style={{ fontSize: 12, color: colors.textSecondary }}>
|
||||
• {cfg.category} — {cfg.all_products
|
||||
? `tous les produits · ${cfg.tiers.map((t) => `${t.buy_quantity}→+${t.free_quantity}`).join(", ") || "aucun seuil"}`
|
||||
: `${cfg.products.length} produit(s) : ${cfg.products.map((pq) => `#${pq.product_id}[${pq.tiers.map((t) => `${t.buy_quantity}→+${t.free_quantity}`).join(",")}]`).join(", ")}`}
|
||||
</Text>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</AccordionSection>
|
||||
);
|
||||
}
|
||||
|
||||
// Palette violette d'origine de l'application (thème par défaut historique)
|
||||
const ORIGINAL_THEME_COLORS = {
|
||||
admin_color_primary: "#7c3aed",
|
||||
@@ -1577,6 +1976,8 @@ export default function SettingsScreen() {
|
||||
points_reward: null,
|
||||
promotions_enabled: false,
|
||||
promotions: [],
|
||||
free_gifts_enabled: false,
|
||||
free_gifts: [],
|
||||
admin_color_primary: "#7c3aed",
|
||||
admin_color_secondary: "#22d3ee",
|
||||
admin_color_success: "#4ade80",
|
||||
@@ -1661,6 +2062,15 @@ export default function SettingsScreen() {
|
||||
...cfg,
|
||||
products: cfg.products ?? [],
|
||||
})),
|
||||
free_gifts_enabled: s.free_gifts_enabled ?? false,
|
||||
free_gifts: (s.free_gifts ?? []).map((cfg) => ({
|
||||
...cfg,
|
||||
tiers: cfg.tiers ?? [],
|
||||
products: (cfg.products ?? []).map((pq) => ({
|
||||
...pq,
|
||||
tiers: pq.tiers ?? [],
|
||||
})),
|
||||
})),
|
||||
});
|
||||
}
|
||||
if (categoriesRes) {
|
||||
@@ -2288,6 +2698,18 @@ export default function SettingsScreen() {
|
||||
s={s}
|
||||
/>
|
||||
|
||||
{/* Offres "achetez X, Y offert" — quantité offerte du même produit */}
|
||||
<FreeGiftsSection
|
||||
enabled={settings.free_gifts_enabled ?? false}
|
||||
freeGifts={settings.free_gifts ?? []}
|
||||
allCategories={categories}
|
||||
productsByCategory={productsByCategory}
|
||||
onToggle={(v) => setSettings((p) => ({ ...p, free_gifts_enabled: v }))}
|
||||
onChangeFreeGifts={(free_gifts) => setSettings((p) => ({ ...p, free_gifts }))}
|
||||
colors={colors}
|
||||
s={s}
|
||||
/>
|
||||
|
||||
{/* Horaires de livraison */}
|
||||
<DeliveryScheduleSection
|
||||
schedule={settings.delivery_schedule ?? DEFAULT_DELIVERY_SCHEDULE}
|
||||
|
||||
Reference in New Issue
Block a user