This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
"expo": {
|
"expo": {
|
||||||
"name": "Admin Panel",
|
"name": "Admin Panel",
|
||||||
"slug": "frontend-admin",
|
"slug": "frontend-admin",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
"icon": "./assets/icon.png",
|
"icon": "./assets/icon.png",
|
||||||
"userInterfaceStyle": "dark",
|
"userInterfaceStyle": "dark",
|
||||||
|
|||||||
@@ -819,29 +819,44 @@ function CentralRewardSection({
|
|||||||
const update = (patch: Partial<PointsReward>) =>
|
const update = (patch: Partial<PointsReward>) =>
|
||||||
onChange({ ...r, ...patch });
|
onChange({ ...r, ...patch });
|
||||||
|
|
||||||
const getCatConfig = (catName: string): RewardCategoryConfig =>
|
// Une catégorie peut avoir jusqu'à deux configs actives en parallèle
|
||||||
r.category_configs.find((c) => c.category === catName) ??
|
// (une "free_product" et une "half_price_product"), chacune avec sa
|
||||||
{ category: catName, type: "free_product", all_products: true, product_ids: [] };
|
// 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: [] };
|
||||||
|
|
||||||
|
const isTypeEnabled = (catName: string, type: RewardCategoryConfig["type"]) =>
|
||||||
|
r.category_configs.some((c) => c.category === catName && c.type === type);
|
||||||
|
|
||||||
const isCatSelected = (catName: string) =>
|
const isCatSelected = (catName: string) =>
|
||||||
r.category_configs.some((c) => c.category === catName);
|
r.category_configs.some((c) => c.category === catName);
|
||||||
|
|
||||||
const toggleCategory = (catName: string) => {
|
const toggleCategoryType = (catName: string, type: RewardCategoryConfig["type"]) => {
|
||||||
if (isCatSelected(catName)) {
|
if (isTypeEnabled(catName, type)) {
|
||||||
update({ category_configs: r.category_configs.filter((c) => c.category !== catName) });
|
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: "free_product", all_products: true, product_ids: [] }] });
|
update({ category_configs: [...r.category_configs, { category: catName, type, all_products: true, product_ids: [] }] });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateCatConfig = (cfg: RewardCategoryConfig) => {
|
const updateCatConfig = (cfg: RewardCategoryConfig) => {
|
||||||
update({
|
update({
|
||||||
category_configs: r.category_configs.map((c) =>
|
category_configs: r.category_configs.map((c) =>
|
||||||
c.category === cfg.category ? cfg : c
|
c.category === cfg.category && c.type === cfg.type ? cfg : c
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 rewardBadge = (
|
const rewardBadge = (
|
||||||
<View style={{
|
<View style={{
|
||||||
paddingHorizontal: spacing.s, paddingVertical: 2, borderRadius: 10,
|
paddingHorizontal: spacing.s, paddingVertical: 2, borderRadius: 10,
|
||||||
@@ -1040,7 +1055,7 @@ function CentralRewardSection({
|
|||||||
<View>
|
<View>
|
||||||
<Text style={[s.rowLabel, { marginBottom: spacing.xs }]}>Catégories éligibles</Text>
|
<Text style={[s.rowLabel, { marginBottom: spacing.xs }]}>Catégories éligibles</Text>
|
||||||
<Text style={[s.rowDesc, { marginBottom: spacing.m }]}>
|
<Text style={[s.rowDesc, { marginBottom: spacing.m }]}>
|
||||||
Sélectionnez les catégories, choisissez le type de récompense pour chacune, puis tous les produits ou une sélection.
|
Sélectionnez une catégorie pour activer, indépendamment, un lot de produits offerts et/ou un lot de produits à -50%, chacun avec sa propre sélection de produits.
|
||||||
</Text>
|
</Text>
|
||||||
{allCategories.length === 0 ? (
|
{allCategories.length === 0 ? (
|
||||||
<Text style={[s.hint, { paddingHorizontal: 0 }]}>Aucune catégorie disponible</Text>
|
<Text style={[s.hint, { paddingHorizontal: 0 }]}>Aucune catégorie disponible</Text>
|
||||||
@@ -1048,12 +1063,13 @@ function CentralRewardSection({
|
|||||||
<View style={{ gap: spacing.m }}>
|
<View style={{ gap: spacing.m }}>
|
||||||
{allCategories.map((cat) => {
|
{allCategories.map((cat) => {
|
||||||
const selected = isCatSelected(cat.name);
|
const selected = isCatSelected(cat.name);
|
||||||
|
const expanded = expandedCats.has(cat.name);
|
||||||
const catColor = cat.color || REWARD_ACCENT;
|
const catColor = cat.color || REWARD_ACCENT;
|
||||||
return (
|
return (
|
||||||
<View key={cat.name}>
|
<View key={cat.name}>
|
||||||
{/* Chip catégorie */}
|
{/* Chip catégorie (couleur = au moins un type actif, clic = déplier/replier) */}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => toggleCategory(cat.name)}
|
onPress={() => toggleExpanded(cat.name)}
|
||||||
style={{
|
style={{
|
||||||
flexDirection: "row", alignItems: "center", gap: spacing.xs,
|
flexDirection: "row", alignItems: "center", gap: spacing.xs,
|
||||||
alignSelf: "flex-start",
|
alignSelf: "flex-start",
|
||||||
@@ -1068,46 +1084,52 @@ function CentralRewardSection({
|
|||||||
{cat.name}
|
{cat.name}
|
||||||
</Text>
|
</Text>
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name={selected ? "chevron-down" : "chevron-forward"}
|
name={expanded ? "chevron-down" : "chevron-forward"}
|
||||||
size={12}
|
size={12}
|
||||||
color={selected ? catColor : colors.textMuted}
|
color={selected ? catColor : colors.textMuted}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
{/* Type + sélecteur produits (visible si catégorie sélectionnée) */}
|
{/* Les deux types de récompense, activables indépendamment */}
|
||||||
{selected && (
|
{expanded && (
|
||||||
<View style={{ marginTop: spacing.s, paddingLeft: spacing.m, gap: spacing.s }}>
|
<View style={{ marginTop: spacing.s, paddingLeft: spacing.m, gap: spacing.m }}>
|
||||||
<View style={{ flexDirection: "row", flexWrap: "wrap", gap: spacing.xs }}>
|
{REWARD_TYPES.map((rt) => {
|
||||||
{REWARD_TYPES.map((rt) => {
|
const typeEnabled = isTypeEnabled(cat.name, rt.value);
|
||||||
const cfg = getCatConfig(cat.name);
|
return (
|
||||||
const sel = (cfg.type || "free_product") === rt.value;
|
<View key={rt.value} style={{ gap: spacing.s }}>
|
||||||
return (
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={rt.value}
|
onPress={() => toggleCategoryType(cat.name, rt.value)}
|
||||||
onPress={() => updateCatConfig({ ...cfg, type: rt.value })}
|
|
||||||
style={{
|
style={{
|
||||||
flexDirection: "row", alignItems: "center", gap: 4,
|
flexDirection: "row", alignItems: "center", gap: 4,
|
||||||
|
alignSelf: "flex-start",
|
||||||
paddingHorizontal: spacing.s, paddingVertical: 4,
|
paddingHorizontal: spacing.s, paddingVertical: 4,
|
||||||
borderRadius: borderRadius.sm, borderWidth: 1.5,
|
borderRadius: borderRadius.sm, borderWidth: 1.5,
|
||||||
borderColor: sel ? REWARD_ACCENT : colors.border,
|
borderColor: typeEnabled ? REWARD_ACCENT : colors.border,
|
||||||
backgroundColor: sel ? REWARD_ACCENT + "22" : "transparent",
|
backgroundColor: typeEnabled ? REWARD_ACCENT + "22" : "transparent",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Ionicons name={rt.icon as any} size={12} color={sel ? REWARD_ACCENT : colors.textMuted} />
|
<Ionicons
|
||||||
<Text style={{ fontSize: 12, fontWeight: sel ? "700" : "400", color: sel ? REWARD_ACCENT : colors.textMuted }}>
|
name={typeEnabled ? "checkbox" : "square-outline"}
|
||||||
|
size={14}
|
||||||
|
color={typeEnabled ? REWARD_ACCENT : colors.textMuted}
|
||||||
|
/>
|
||||||
|
<Ionicons name={rt.icon as any} size={12} color={typeEnabled ? REWARD_ACCENT : colors.textMuted} />
|
||||||
|
<Text style={{ fontSize: 12, fontWeight: typeEnabled ? "700" : "400", color: typeEnabled ? REWARD_ACCENT : colors.textMuted }}>
|
||||||
{rt.label}
|
{rt.label}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
{typeEnabled && (
|
||||||
})}
|
<CategoryProductPicker
|
||||||
</View>
|
catConfig={getCatConfig(cat.name, rt.value)}
|
||||||
<CategoryProductPicker
|
products={productsByCategory[cat.name] ?? []}
|
||||||
catConfig={getCatConfig(cat.name)}
|
onChange={updateCatConfig}
|
||||||
products={productsByCategory[cat.name] ?? []}
|
colors={colors}
|
||||||
onChange={updateCatConfig}
|
s={s}
|
||||||
colors={colors}
|
/>
|
||||||
s={s}
|
)}
|
||||||
/>
|
</View>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
@@ -1127,8 +1149,8 @@ function CentralRewardSection({
|
|||||||
{r.description !== "" && (
|
{r.description !== "" && (
|
||||||
<Text style={{ fontSize: 12, color: colors.textMuted, fontStyle: "italic" }}>"{r.description}"</Text>
|
<Text style={{ fontSize: 12, color: colors.textMuted, fontStyle: "italic" }}>"{r.description}"</Text>
|
||||||
)}
|
)}
|
||||||
{r.category_configs.map((cfg) => (
|
{r.category_configs.map((cfg, idx) => (
|
||||||
<Text key={cfg.category} 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)`}
|
• {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)`}
|
||||||
</Text>
|
</Text>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user