chore: build
This commit is contained in:
@@ -264,17 +264,6 @@ export const notifyClientToDescend = async (commandId: number) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const confirmReceptionAdmin = async (commandId: number) => {
|
||||
const { data } = await apiClient.post(
|
||||
`${V2}/admin/protected/orders/${commandId}/confirm-reception`,
|
||||
);
|
||||
return {
|
||||
success: true,
|
||||
message: data.message,
|
||||
points_earned: data.points_earned,
|
||||
client_username: data.client_username,
|
||||
};
|
||||
};
|
||||
|
||||
// ============================================
|
||||
|
||||
@@ -935,13 +924,18 @@ export interface RewardCategoryConfig {
|
||||
amount: number;
|
||||
}
|
||||
|
||||
export interface RewardItem {
|
||||
product_id: number;
|
||||
quantity: number;
|
||||
price: number;
|
||||
}
|
||||
|
||||
export interface PointsReward {
|
||||
threshold: number;
|
||||
type: "free_product" | "half_price_product" | "custom";
|
||||
description: string;
|
||||
category_configs: RewardCategoryConfig[];
|
||||
reward_product_id?: number;
|
||||
reward_quantity?: number;
|
||||
reward_items: RewardItem[];
|
||||
}
|
||||
|
||||
export interface PointsPool {
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
getCommandItems,
|
||||
updateCommandStatus,
|
||||
validateCommand,
|
||||
confirmReceptionAdmin,
|
||||
notifyClientToDescend,
|
||||
getDeliveryPersonDetails,
|
||||
deleteCommandItem,
|
||||
@@ -206,20 +205,6 @@ export default function OrderDetailScreen() {
|
||||
);
|
||||
};
|
||||
|
||||
const handleConfirmReception = async () => {
|
||||
try {
|
||||
const res = await confirmReceptionAdmin(orderId);
|
||||
showSuccess(
|
||||
"Réception confirmée",
|
||||
`${res.points_earned} point(s) attribués au client ${res.client_username}`,
|
||||
);
|
||||
const updated = await getCommandByID(orderId);
|
||||
setCommand(updated.command);
|
||||
} catch (e: any) {
|
||||
showError("Erreur", e.message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteCommand = () => {
|
||||
Alert.alert(
|
||||
"Supprimer la commande",
|
||||
@@ -880,15 +865,7 @@ export default function OrderDetailScreen() {
|
||||
style={{ marginTop: spacing.s }}
|
||||
/>
|
||||
)}
|
||||
{!["approved", "cancelled"].includes(command.status) && (
|
||||
<Button
|
||||
title="Confirmer la commande"
|
||||
onPress={handleConfirmReception}
|
||||
variant="primary"
|
||||
fullWidth
|
||||
style={{ marginTop: spacing.s }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button
|
||||
title="Supprimer la commande"
|
||||
onPress={handleDeleteCommand}
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
updateCommandStatus,
|
||||
getCommandItems,
|
||||
notifyClientToDescend,
|
||||
confirmReceptionAdmin,
|
||||
|
||||
validateCommand,
|
||||
deleteCommand,
|
||||
deleteCommandItem,
|
||||
@@ -221,26 +221,6 @@ export default function OrdersScreen() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmReception = (commandId: number) => {
|
||||
showConfirm(
|
||||
"Confirmer la commande",
|
||||
`Confirmer la réception de la commande #${commandId} ?`,
|
||||
async () => {
|
||||
try {
|
||||
const res = await confirmReceptionAdmin(commandId);
|
||||
showSuccess(
|
||||
"Réception confirmée",
|
||||
`${res.points_earned} point(s) attribués au client ${res.client_username}`,
|
||||
);
|
||||
await loadData();
|
||||
} catch (e: any) {
|
||||
showError("Erreur", e.message);
|
||||
}
|
||||
},
|
||||
"Confirmer",
|
||||
);
|
||||
};
|
||||
|
||||
const handleForceValidate = (commandId: number) => {
|
||||
showConfirm(
|
||||
"Finaliser la commande",
|
||||
@@ -794,15 +774,7 @@ export default function OrdersScreen() {
|
||||
},
|
||||
condition: item.status === "assigned",
|
||||
},
|
||||
{
|
||||
label: "Confirmer réception",
|
||||
icon: "checkmark-circle-outline" as keyof typeof Ionicons.glyphMap,
|
||||
onPress: () => {
|
||||
setOpenMenuId(null);
|
||||
handleConfirmReception(item.id);
|
||||
},
|
||||
condition: item.status === "livre",
|
||||
},
|
||||
|
||||
{
|
||||
label: "Finaliser commande",
|
||||
icon: "trophy-outline" as keyof typeof Ionicons.glyphMap,
|
||||
|
||||
@@ -16,7 +16,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, DaySchedule, DeliverySchedule, PostalZone } from "../../api/api_admin";
|
||||
import type { AppSettings, Category, CategoryRoute, DeliveryModeConfig, PointsTier, PointsPool, PointsReward, RewardCategoryConfig, RewardItem, DaySchedule, DeliverySchedule, PostalZone } from "../../api/api_admin";
|
||||
import type { Product } from "../../api/types";
|
||||
import AlertModal from "../../components/ui/AlertModal";
|
||||
import { useAlert } from "../../hooks/useAlert";
|
||||
@@ -46,6 +46,51 @@ const DAYS: { key: keyof DeliverySchedule; label: string }[] = [
|
||||
{ key: "sunday", label: "Dimanche" },
|
||||
];
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// Composant section accordéon générique
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
function AccordionSection({
|
||||
title,
|
||||
badge,
|
||||
children,
|
||||
colors,
|
||||
s,
|
||||
}: {
|
||||
title: string;
|
||||
badge?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
colors: any;
|
||||
s: any;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<View style={s.section}>
|
||||
<TouchableOpacity
|
||||
onPress={() => setOpen((o) => !o)}
|
||||
activeOpacity={0.7}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
paddingHorizontal: spacing.l,
|
||||
paddingVertical: spacing.m,
|
||||
}}
|
||||
>
|
||||
<Text style={[s.sectionTitle, { paddingHorizontal: 0, paddingTop: 0, paddingBottom: 0 }]}>{title}</Text>
|
||||
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.s }}>
|
||||
{badge}
|
||||
<Ionicons name={open ? "chevron-up" : "chevron-down"} size={18} color={colors.textMuted} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
{open && (
|
||||
<View style={{ borderTopWidth: 1, borderTopColor: colors.border }}>
|
||||
{children}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function DeliveryScheduleSection({
|
||||
schedule,
|
||||
onChange,
|
||||
@@ -63,8 +108,7 @@ function DeliveryScheduleSection({
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={s.section}>
|
||||
<Text style={s.sectionTitle}>Horaires de livraison</Text>
|
||||
<AccordionSection title="Horaires de livraison" colors={colors} s={s}>
|
||||
<Text style={[s.hint, { paddingTop: spacing.s }]}>
|
||||
Définissez les jours et horaires d'ouverture pour la livraison.
|
||||
</Text>
|
||||
@@ -133,7 +177,7 @@ function DeliveryScheduleSection({
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</AccordionSection>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -200,8 +244,7 @@ function PostalZonesSection({
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={s.section}>
|
||||
<Text style={s.sectionTitle}>Zones de livraison</Text>
|
||||
<AccordionSection title="Zones de livraison" colors={colors} s={s}>
|
||||
<Text style={[s.hint, { paddingTop: spacing.s }]}>
|
||||
Définissez les zones de livraison et leur montant minimum de commande.
|
||||
</Text>
|
||||
@@ -436,7 +479,7 @@ function PostalZonesSection({
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</AccordionSection>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -547,24 +590,20 @@ function TiersSection({
|
||||
onChange(tiers.filter((_, idx) => idx !== i));
|
||||
};
|
||||
|
||||
const badge = (
|
||||
<View style={{
|
||||
paddingHorizontal: spacing.s, paddingVertical: 2, borderRadius: 10,
|
||||
backgroundColor: active ? accentColor + "25" : colors.border + "40",
|
||||
borderWidth: 1, borderColor: active ? accentColor : colors.border,
|
||||
}}>
|
||||
<Text style={{ fontSize: fontSize.xs, fontWeight: "700", color: active ? accentColor : colors.textMuted }}>
|
||||
{active ? "Actif" : "Ignoré"}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={s.section}>
|
||||
<View style={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between", paddingRight: spacing.l }}>
|
||||
<Text style={s.sectionTitle}>{title}</Text>
|
||||
<View style={{
|
||||
paddingHorizontal: spacing.s,
|
||||
paddingVertical: 2,
|
||||
borderRadius: 10,
|
||||
backgroundColor: active ? accentColor + "25" : colors.border + "40",
|
||||
borderWidth: 1,
|
||||
borderColor: active ? accentColor : colors.border,
|
||||
marginBottom: spacing.s,
|
||||
}}>
|
||||
<Text style={{ fontSize: fontSize.xs, fontWeight: "700", color: active ? accentColor : colors.textMuted }}>
|
||||
{active ? "Actif" : "Ignoré"}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<AccordionSection title={title} badge={badge} colors={colors} s={s}>
|
||||
<Text style={s.hint}>
|
||||
Définissez chaque palier : de X€ à Y€ = N points.{"\n"}
|
||||
Max = 0 signifie illimité (pas de borne supérieure).
|
||||
@@ -640,7 +679,7 @@ function TiersSection({
|
||||
Ajouter un palier
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</AccordionSection>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -657,8 +696,7 @@ const EMPTY_REWARD: PointsReward = {
|
||||
type: "free_product",
|
||||
description: "",
|
||||
category_configs: [],
|
||||
reward_product_id: 0,
|
||||
reward_quantity: 0,
|
||||
reward_items: [],
|
||||
};
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
@@ -826,22 +864,20 @@ function CentralRewardSection({
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={s.section}>
|
||||
<View style={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between", paddingRight: spacing.l }}>
|
||||
<Text style={s.sectionTitle}>Récompenses par palier</Text>
|
||||
<View style={{
|
||||
paddingHorizontal: spacing.s, paddingVertical: 2, borderRadius: 10,
|
||||
backgroundColor: enabled ? REWARD_ACCENT + "25" : colors.border + "40",
|
||||
borderWidth: 1, borderColor: enabled ? REWARD_ACCENT : colors.border,
|
||||
marginBottom: spacing.s,
|
||||
}}>
|
||||
<Text style={{ fontSize: 10, fontWeight: "700", color: enabled ? REWARD_ACCENT : colors.textMuted }}>
|
||||
{enabled ? "Activée" : "Désactivée"}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
const rewardBadge = (
|
||||
<View style={{
|
||||
paddingHorizontal: spacing.s, paddingVertical: 2, borderRadius: 10,
|
||||
backgroundColor: enabled ? REWARD_ACCENT + "25" : colors.border + "40",
|
||||
borderWidth: 1, borderColor: enabled ? REWARD_ACCENT : colors.border,
|
||||
}}>
|
||||
<Text style={{ fontSize: 10, fontWeight: "700", color: enabled ? REWARD_ACCENT : colors.textMuted }}>
|
||||
{enabled ? "Activée" : "Désactivée"}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<AccordionSection title="Récompenses par palier" badge={rewardBadge} colors={colors} s={s}>
|
||||
<View style={[s.row, s.rowFirst]}>
|
||||
<View style={s.rowLeft}>
|
||||
<Text style={s.rowLabel}>Récompense activée</Text>
|
||||
@@ -920,72 +956,129 @@ function CentralRewardSection({
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Produit récompense */}
|
||||
{/* Produits récompense */}
|
||||
<View>
|
||||
<Text style={[s.rowLabel, { marginBottom: spacing.xs }]}>Produit ajouté au panier</Text>
|
||||
<Text style={[s.rowLabel, { marginBottom: spacing.xs }]}>Produits ajoutés au panier</Text>
|
||||
<Text style={[s.rowDesc, { marginBottom: spacing.m }]}>
|
||||
Quand le client réclame sa récompense, ce produit est automatiquement ajouté à son panier (gratuit). Il doit commander au moins un produit normal.
|
||||
Quand le client réclame sa récompense, ces produits sont automatiquement ajoutés à son panier (gratuits). Il doit commander au moins un produit normal.
|
||||
</Text>
|
||||
{/* Sélecteur produit */}
|
||||
<View style={{ gap: spacing.s }}>
|
||||
{r.reward_items.map((item, idx) => {
|
||||
const allProds = Object.values(productsByCategory).flat();
|
||||
const prod = allProds.find((p) => p.id === item.product_id);
|
||||
return (
|
||||
<View
|
||||
key={idx}
|
||||
style={{
|
||||
borderWidth: 1, borderColor: REWARD_ACCENT + "44",
|
||||
borderRadius: borderRadius.sm, padding: spacing.m,
|
||||
backgroundColor: REWARD_ACCENT + "08", gap: spacing.s,
|
||||
}}
|
||||
>
|
||||
{/* Sélecteur produit */}
|
||||
<View style={{ flexDirection: "row", flexWrap: "wrap", gap: spacing.xs }}>
|
||||
{allProds.map((p) => {
|
||||
const sel = item.product_id === p.id;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={p.id}
|
||||
onPress={() => {
|
||||
const updated = r.reward_items.map((it, i) =>
|
||||
i === idx ? { ...it, product_id: p.id } : it
|
||||
);
|
||||
update({ reward_items: updated });
|
||||
}}
|
||||
style={{
|
||||
flexDirection: "row", alignItems: "center", gap: 4,
|
||||
paddingHorizontal: spacing.s, paddingVertical: 4,
|
||||
borderRadius: borderRadius.sm, borderWidth: 1.5,
|
||||
borderColor: sel ? REWARD_ACCENT : colors.border,
|
||||
backgroundColor: sel ? REWARD_ACCENT + "22" : "transparent",
|
||||
}}
|
||||
>
|
||||
{sel && <Ionicons name="checkmark" size={11} color={REWARD_ACCENT} />}
|
||||
<Text style={{ fontSize: 12, fontWeight: sel ? "700" : "400", color: sel ? REWARD_ACCENT : colors.textMuted }}>
|
||||
{p.name} ({p.category})
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
|
||||
{/* Quantité + Prix + Supprimer */}
|
||||
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.m }}>
|
||||
<View style={{ flex: 1, flexDirection: "row", alignItems: "center", gap: spacing.xs }}>
|
||||
<Text style={{ fontSize: 12, color: colors.textMuted }}>Qté :</Text>
|
||||
<TextInput
|
||||
style={[s.thresholdInput, { width: 56, fontSize: 13 }]}
|
||||
keyboardType="decimal-pad"
|
||||
value={item.quantity > 0 ? String(item.quantity) : ""}
|
||||
onChangeText={(v) => {
|
||||
const n = parseFloat(v);
|
||||
const updated = r.reward_items.map((it, i) =>
|
||||
i === idx ? { ...it, quantity: isNaN(n) ? 0 : n } : it
|
||||
);
|
||||
update({ reward_items: updated });
|
||||
}}
|
||||
placeholder="1"
|
||||
placeholderTextColor={colors.textMuted}
|
||||
/>
|
||||
</View>
|
||||
<View style={{ flex: 1, flexDirection: "row", alignItems: "center", gap: spacing.xs }}>
|
||||
<Text style={{ fontSize: 12, color: colors.textMuted }}>Prix :</Text>
|
||||
<TextInput
|
||||
style={[s.thresholdInput, { width: 56, fontSize: 13 }]}
|
||||
keyboardType="decimal-pad"
|
||||
value={item.price > 0 ? String(item.price) : ""}
|
||||
onChangeText={(v) => {
|
||||
const n = parseFloat(v);
|
||||
const updated = r.reward_items.map((it, i) =>
|
||||
i === idx ? { ...it, price: isNaN(n) ? 0 : n } : it
|
||||
);
|
||||
update({ reward_items: updated });
|
||||
}}
|
||||
placeholder="0"
|
||||
placeholderTextColor={colors.textMuted}
|
||||
/>
|
||||
<Text style={{ fontSize: 12, color: colors.textMuted }}>€</Text>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
onPress={() => update({ reward_items: r.reward_items.filter((_, i) => i !== idx) })}
|
||||
style={{ padding: spacing.xs }}
|
||||
>
|
||||
<Ionicons name="trash-outline" size={18} color="#ef4444" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{prod && (
|
||||
<Text style={{ fontSize: 11, color: REWARD_ACCENT, fontStyle: "italic" }}>
|
||||
{prod.name}{item.quantity > 0 ? ` · x${item.quantity}` : ""}{item.price > 0 ? ` · ${item.price}€` : ""}
|
||||
</Text>
|
||||
)}
|
||||
{!prod && (
|
||||
<Text style={{ fontSize: 11, color: colors.textMuted, fontStyle: "italic" }}>
|
||||
Sélectionnez un produit ci-dessus
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
|
||||
{Object.values(productsByCategory).flat().length === 0 ? (
|
||||
<Text style={[s.hint, { paddingHorizontal: 0 }]}>Aucun produit disponible</Text>
|
||||
) : (
|
||||
<View style={{ gap: spacing.xs }}>
|
||||
<TouchableOpacity
|
||||
onPress={() => update({ reward_product_id: 0, reward_quantity: 0 })}
|
||||
style={{
|
||||
flexDirection: "row", alignItems: "center", gap: spacing.xs,
|
||||
paddingHorizontal: spacing.m, paddingVertical: spacing.s,
|
||||
borderRadius: borderRadius.sm, borderWidth: 1.5,
|
||||
borderColor: !r.reward_product_id ? colors.border : "transparent",
|
||||
backgroundColor: !r.reward_product_id ? colors.border + "30" : "transparent",
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontSize: 13, color: !r.reward_product_id ? colors.textPrimary : colors.textMuted }}>
|
||||
Aucun (désactivé)
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
{Object.values(productsByCategory).flat().map((prod) => {
|
||||
const sel = r.reward_product_id === prod.id;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={prod.id}
|
||||
onPress={() => update({ reward_product_id: prod.id, reward_quantity: r.reward_quantity && r.reward_quantity > 0 ? r.reward_quantity : 1 })}
|
||||
style={{
|
||||
flexDirection: "row", alignItems: "center", gap: spacing.xs,
|
||||
paddingHorizontal: spacing.m, paddingVertical: spacing.s,
|
||||
borderRadius: borderRadius.sm, borderWidth: 1.5,
|
||||
borderColor: sel ? REWARD_ACCENT : colors.border,
|
||||
backgroundColor: sel ? REWARD_ACCENT + "18" : "transparent",
|
||||
}}
|
||||
>
|
||||
<Ionicons name="gift-outline" size={14} color={sel ? REWARD_ACCENT : colors.textMuted} />
|
||||
<Text style={{ flex: 1, fontSize: 13, fontWeight: sel ? "700" : "400", color: sel ? REWARD_ACCENT : colors.textMuted }}>
|
||||
{prod.name} ({prod.category})
|
||||
</Text>
|
||||
{sel && <Ionicons name="checkmark-circle" size={16} color={REWARD_ACCENT} />}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
)}
|
||||
{/* Quantité */}
|
||||
{!!r.reward_product_id && (
|
||||
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.m, marginTop: spacing.s }}>
|
||||
<Text style={[s.rowLabel, { flex: 1 }]}>Quantité offerte</Text>
|
||||
<TextInput
|
||||
style={[s.thresholdInput, { width: 64 }]}
|
||||
keyboardType="decimal-pad"
|
||||
value={r.reward_quantity ? String(r.reward_quantity) : ""}
|
||||
onChangeText={(v) => {
|
||||
const n = parseFloat(v);
|
||||
if (!isNaN(n) && n > 0) update({ reward_quantity: n });
|
||||
}}
|
||||
placeholder="1"
|
||||
placeholderTextColor={colors.textMuted}
|
||||
/>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
onPress={() => update({ reward_items: [...r.reward_items, { product_id: 0, quantity: 1, price: 0 }] })}
|
||||
style={{
|
||||
flexDirection: "row", alignItems: "center", justifyContent: "center", gap: spacing.xs,
|
||||
paddingHorizontal: spacing.m, paddingVertical: spacing.s,
|
||||
borderRadius: borderRadius.sm, borderWidth: 1.5,
|
||||
borderColor: REWARD_ACCENT + "66",
|
||||
}}
|
||||
>
|
||||
<Ionicons name="add-circle-outline" size={16} color={REWARD_ACCENT} />
|
||||
<Text style={{ fontSize: 13, color: REWARD_ACCENT, fontWeight: "600" }}>Ajouter un produit récompense</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
@@ -1046,7 +1139,7 @@ function CentralRewardSection({
|
||||
</View>
|
||||
|
||||
{/* Récapitulatif */}
|
||||
{r.category_configs.length > 0 && (
|
||||
{(r.category_configs.length > 0 || r.reward_items.length > 0) && (
|
||||
<View style={{ backgroundColor: REWARD_ACCENT + "12", borderRadius: borderRadius.sm, borderLeftWidth: 3, borderLeftColor: REWARD_ACCENT, padding: spacing.m, gap: 4 }}>
|
||||
<Text style={{ fontSize: 13, fontWeight: "700", color: REWARD_ACCENT }}>Récapitulatif</Text>
|
||||
<Text style={{ fontSize: 13, color: colors.textPrimary }}>
|
||||
@@ -1058,14 +1151,22 @@ function CentralRewardSection({
|
||||
)}
|
||||
{r.category_configs.map((cfg) => (
|
||||
<Text key={cfg.category} style={{ fontSize: 12, color: colors.textSecondary }}>
|
||||
• {cfg.category}{cfg.amount > 0 ? ` (${cfg.amount}€)` : ""} : {cfg.all_products ? "tous les produits" : `${cfg.product_ids.length} produit(s) sélectionné(s)`}
|
||||
• Éligible : {cfg.category}{cfg.amount > 0 ? ` (${cfg.amount}€)` : ""} — {cfg.all_products ? "tous les produits" : `${cfg.product_ids.length} produit(s)`}
|
||||
</Text>
|
||||
))}
|
||||
{r.reward_items.filter((it) => it.product_id > 0).map((it, idx) => {
|
||||
const prod = Object.values(productsByCategory).flat().find((p) => p.id === it.product_id);
|
||||
return (
|
||||
<Text key={idx} style={{ fontSize: 12, color: colors.textSecondary }}>
|
||||
🎁 {prod?.name ?? `Produit #${it.product_id}`}{it.quantity > 0 ? ` · x${it.quantity}` : ""}{it.price > 0 ? ` · ${it.price}€` : ""}
|
||||
</Text>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</AccordionSection>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1167,7 +1268,7 @@ 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 ?? [], reward_items: s.points_reward.reward_items ?? [] }
|
||||
: null,
|
||||
});
|
||||
}
|
||||
@@ -1397,8 +1498,7 @@ export default function SettingsScreen() {
|
||||
<View style={s.container}>
|
||||
<ScrollView contentContainerStyle={s.content}>
|
||||
{/* Personnalisation */}
|
||||
<View style={s.section}>
|
||||
<Text style={s.sectionTitle}>Personnalisation</Text>
|
||||
<AccordionSection title="Personnalisation" colors={colors} s={s}>
|
||||
<View style={[s.row, s.rowFirst]}>
|
||||
<View style={s.rowLeft}>
|
||||
<Text style={s.rowLabel}>Nom du shop</Text>
|
||||
@@ -1415,11 +1515,10 @@ export default function SettingsScreen() {
|
||||
autoCorrect={false}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</AccordionSection>
|
||||
|
||||
{/* Amendes */}
|
||||
<View style={s.section}>
|
||||
<Text style={s.sectionTitle}>Amendes</Text>
|
||||
<AccordionSection title="Amendes" colors={colors} s={s}>
|
||||
<View style={[s.row, s.rowFirst]}>
|
||||
<View style={s.rowLeft}>
|
||||
<Text style={s.rowLabel}>Amendes activées</Text>
|
||||
@@ -1522,11 +1621,10 @@ export default function SettingsScreen() {
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</AccordionSection>
|
||||
|
||||
{/* Parrainage */}
|
||||
<View style={s.section}>
|
||||
<Text style={s.sectionTitle}>Parrainage</Text>
|
||||
<AccordionSection title="Parrainage" colors={colors} s={s}>
|
||||
<View style={[s.row, s.rowFirst]}>
|
||||
<View style={s.rowLeft}>
|
||||
<Text style={s.rowLabel}>Parrainage activé</Text>
|
||||
@@ -1544,11 +1642,10 @@ export default function SettingsScreen() {
|
||||
thumbColor="#fff"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</AccordionSection>
|
||||
|
||||
{/* Système de points */}
|
||||
<View style={s.section}>
|
||||
<Text style={s.sectionTitle}>Système de points</Text>
|
||||
<AccordionSection title="Système de points" colors={colors} s={s}>
|
||||
<View style={[s.row, s.rowFirst]}>
|
||||
<View style={s.rowLeft}>
|
||||
<Text style={s.rowLabel}>Points activés</Text>
|
||||
@@ -1604,11 +1701,10 @@ export default function SettingsScreen() {
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</AccordionSection>
|
||||
|
||||
{/* Attribution des catégories */}
|
||||
<View style={s.section}>
|
||||
<Text style={s.sectionTitle}>Attribution des catégories aux points</Text>
|
||||
<AccordionSection title="Attribution des catégories aux points" colors={colors} s={s}>
|
||||
<Text style={s.hint}>
|
||||
Choisissez quel type de point est attribué pour chaque catégorie de produit.{"\n"}
|
||||
Vous pouvez modifier les attributions à tout moment.
|
||||
@@ -1695,7 +1791,7 @@ export default function SettingsScreen() {
|
||||
Ajoutez au moins un type de point pour assigner des catégories.
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</AccordionSection>
|
||||
|
||||
{/* Barèmes de points — un par pool */}
|
||||
{pools.map((pool, i) => (
|
||||
@@ -1738,8 +1834,7 @@ export default function SettingsScreen() {
|
||||
/>
|
||||
|
||||
{/* Paiement crypto */}
|
||||
<View style={s.section}>
|
||||
<Text style={s.sectionTitle}>Paiement crypto</Text>
|
||||
<AccordionSection title="Paiement crypto" colors={colors} s={s}>
|
||||
|
||||
{/* Toggle activation */}
|
||||
<View style={[s.row, s.rowFirst]}>
|
||||
@@ -1881,13 +1976,12 @@ export default function SettingsScreen() {
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</AccordionSection>
|
||||
|
||||
{/* ============================================ */}
|
||||
{/* 🤖 TELEGRAM */}
|
||||
{/* ============================================ */}
|
||||
<View style={s.section}>
|
||||
<Text style={s.sectionTitle}>Notifications Telegram</Text>
|
||||
<AccordionSection title="Notifications Telegram" colors={colors} s={s}>
|
||||
<View style={s.row}>
|
||||
<View style={s.rowLeft}>
|
||||
<Text style={s.rowLabel}>Notifications activées</Text>
|
||||
@@ -1977,13 +2071,12 @@ export default function SettingsScreen() {
|
||||
</View>
|
||||
|
||||
</View>
|
||||
</View>
|
||||
</AccordionSection>
|
||||
|
||||
{/* ============================================ */}
|
||||
{/* 🚚 MODE DE LIVRAISON */}
|
||||
{/* ============================================ */}
|
||||
<View style={s.section}>
|
||||
<Text style={s.sectionTitle}>Mode de livraison</Text>
|
||||
<AccordionSection title="Mode de livraison" colors={colors} s={s}>
|
||||
<Text style={[s.hint, { paddingTop: spacing.s, paddingHorizontal: spacing.l }]}>
|
||||
Choisissez comment les commandes sont assignées aux livreurs.
|
||||
</Text>
|
||||
@@ -2102,7 +2195,7 @@ export default function SettingsScreen() {
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</AccordionSection>
|
||||
|
||||
<TouchableOpacity
|
||||
style={s.saveButton}
|
||||
|
||||
Reference in New Issue
Block a user