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;
|
amount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RewardItem {
|
||||||
|
product_id: number;
|
||||||
|
quantity: number;
|
||||||
|
price: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface PointsReward {
|
export interface PointsReward {
|
||||||
threshold: number;
|
threshold: number;
|
||||||
type: "free_product" | "half_price_product" | "custom";
|
type: "free_product" | "half_price_product" | "custom";
|
||||||
description: string;
|
description: string;
|
||||||
category_configs: RewardCategoryConfig[];
|
category_configs: RewardCategoryConfig[];
|
||||||
reward_product_id?: number;
|
reward_items: RewardItem[];
|
||||||
reward_quantity?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PointsPool {
|
export interface PointsPool {
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import {
|
|||||||
getCommandItems,
|
getCommandItems,
|
||||||
updateCommandStatus,
|
updateCommandStatus,
|
||||||
validateCommand,
|
validateCommand,
|
||||||
confirmReceptionAdmin,
|
|
||||||
notifyClientToDescend,
|
notifyClientToDescend,
|
||||||
getDeliveryPersonDetails,
|
getDeliveryPersonDetails,
|
||||||
deleteCommandItem,
|
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 = () => {
|
const handleDeleteCommand = () => {
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
"Supprimer la commande",
|
"Supprimer la commande",
|
||||||
@@ -880,15 +865,7 @@ export default function OrderDetailScreen() {
|
|||||||
style={{ marginTop: spacing.s }}
|
style={{ marginTop: spacing.s }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!["approved", "cancelled"].includes(command.status) && (
|
|
||||||
<Button
|
|
||||||
title="Confirmer la commande"
|
|
||||||
onPress={handleConfirmReception}
|
|
||||||
variant="primary"
|
|
||||||
fullWidth
|
|
||||||
style={{ marginTop: spacing.s }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Button
|
<Button
|
||||||
title="Supprimer la commande"
|
title="Supprimer la commande"
|
||||||
onPress={handleDeleteCommand}
|
onPress={handleDeleteCommand}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import {
|
|||||||
updateCommandStatus,
|
updateCommandStatus,
|
||||||
getCommandItems,
|
getCommandItems,
|
||||||
notifyClientToDescend,
|
notifyClientToDescend,
|
||||||
confirmReceptionAdmin,
|
|
||||||
validateCommand,
|
validateCommand,
|
||||||
deleteCommand,
|
deleteCommand,
|
||||||
deleteCommandItem,
|
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) => {
|
const handleForceValidate = (commandId: number) => {
|
||||||
showConfirm(
|
showConfirm(
|
||||||
"Finaliser la commande",
|
"Finaliser la commande",
|
||||||
@@ -794,15 +774,7 @@ export default function OrdersScreen() {
|
|||||||
},
|
},
|
||||||
condition: item.status === "assigned",
|
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",
|
label: "Finaliser commande",
|
||||||
icon: "trophy-outline" as keyof typeof Ionicons.glyphMap,
|
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 { spacing, fontSize, borderRadius } from "../../theme";
|
||||||
import { useTheme } from "../../context/ThemeContext";
|
import { useTheme } from "../../context/ThemeContext";
|
||||||
import { getSettings, updateSettings, getCategories, getAvailableDeliveryPersons, getAllProductsAdmin, DEFAULT_DELIVERY_SCHEDULE, DEFAULT_POSTAL_ZONES } from "../../api/api_admin";
|
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 type { Product } from "../../api/types";
|
||||||
import AlertModal from "../../components/ui/AlertModal";
|
import AlertModal from "../../components/ui/AlertModal";
|
||||||
import { useAlert } from "../../hooks/useAlert";
|
import { useAlert } from "../../hooks/useAlert";
|
||||||
@@ -46,6 +46,51 @@ const DAYS: { key: keyof DeliverySchedule; label: string }[] = [
|
|||||||
{ key: "sunday", label: "Dimanche" },
|
{ 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({
|
function DeliveryScheduleSection({
|
||||||
schedule,
|
schedule,
|
||||||
onChange,
|
onChange,
|
||||||
@@ -63,8 +108,7 @@ function DeliveryScheduleSection({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={s.section}>
|
<AccordionSection title="Horaires de livraison" colors={colors} s={s}>
|
||||||
<Text style={s.sectionTitle}>Horaires de livraison</Text>
|
|
||||||
<Text style={[s.hint, { paddingTop: spacing.s }]}>
|
<Text style={[s.hint, { paddingTop: spacing.s }]}>
|
||||||
Définissez les jours et horaires d'ouverture pour la livraison.
|
Définissez les jours et horaires d'ouverture pour la livraison.
|
||||||
</Text>
|
</Text>
|
||||||
@@ -133,7 +177,7 @@ function DeliveryScheduleSection({
|
|||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</View>
|
</AccordionSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,8 +244,7 @@ function PostalZonesSection({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={s.section}>
|
<AccordionSection title="Zones de livraison" colors={colors} s={s}>
|
||||||
<Text style={s.sectionTitle}>Zones de livraison</Text>
|
|
||||||
<Text style={[s.hint, { paddingTop: spacing.s }]}>
|
<Text style={[s.hint, { paddingTop: spacing.s }]}>
|
||||||
Définissez les zones de livraison et leur montant minimum de commande.
|
Définissez les zones de livraison et leur montant minimum de commande.
|
||||||
</Text>
|
</Text>
|
||||||
@@ -436,7 +479,7 @@ function PostalZonesSection({
|
|||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
</View>
|
</AccordionSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -547,24 +590,20 @@ function TiersSection({
|
|||||||
onChange(tiers.filter((_, idx) => idx !== i));
|
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 (
|
return (
|
||||||
<View style={s.section}>
|
<AccordionSection title={title} badge={badge} colors={colors} s={s}>
|
||||||
<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>
|
|
||||||
<Text style={s.hint}>
|
<Text style={s.hint}>
|
||||||
Définissez chaque palier : de X€ à Y€ = N points.{"\n"}
|
Définissez chaque palier : de X€ à Y€ = N points.{"\n"}
|
||||||
Max = 0 signifie illimité (pas de borne supérieure).
|
Max = 0 signifie illimité (pas de borne supérieure).
|
||||||
@@ -640,7 +679,7 @@ function TiersSection({
|
|||||||
Ajouter un palier
|
Ajouter un palier
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</AccordionSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -657,8 +696,7 @@ const EMPTY_REWARD: PointsReward = {
|
|||||||
type: "free_product",
|
type: "free_product",
|
||||||
description: "",
|
description: "",
|
||||||
category_configs: [],
|
category_configs: [],
|
||||||
reward_product_id: 0,
|
reward_items: [],
|
||||||
reward_quantity: 0,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ──────────────────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────────────────
|
||||||
@@ -826,22 +864,20 @@ function CentralRewardSection({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const rewardBadge = (
|
||||||
<View style={s.section}>
|
<View style={{
|
||||||
<View style={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between", paddingRight: spacing.l }}>
|
paddingHorizontal: spacing.s, paddingVertical: 2, borderRadius: 10,
|
||||||
<Text style={s.sectionTitle}>Récompenses par palier</Text>
|
backgroundColor: enabled ? REWARD_ACCENT + "25" : colors.border + "40",
|
||||||
<View style={{
|
borderWidth: 1, borderColor: enabled ? REWARD_ACCENT : colors.border,
|
||||||
paddingHorizontal: spacing.s, paddingVertical: 2, borderRadius: 10,
|
}}>
|
||||||
backgroundColor: enabled ? REWARD_ACCENT + "25" : colors.border + "40",
|
<Text style={{ fontSize: 10, fontWeight: "700", color: enabled ? REWARD_ACCENT : colors.textMuted }}>
|
||||||
borderWidth: 1, borderColor: enabled ? REWARD_ACCENT : colors.border,
|
{enabled ? "Activée" : "Désactivée"}
|
||||||
marginBottom: spacing.s,
|
</Text>
|
||||||
}}>
|
</View>
|
||||||
<Text style={{ fontSize: 10, fontWeight: "700", color: enabled ? REWARD_ACCENT : colors.textMuted }}>
|
);
|
||||||
{enabled ? "Activée" : "Désactivée"}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AccordionSection title="Récompenses par palier" badge={rewardBadge} colors={colors} s={s}>
|
||||||
<View style={[s.row, s.rowFirst]}>
|
<View style={[s.row, s.rowFirst]}>
|
||||||
<View style={s.rowLeft}>
|
<View style={s.rowLeft}>
|
||||||
<Text style={s.rowLabel}>Récompense activée</Text>
|
<Text style={s.rowLabel}>Récompense activée</Text>
|
||||||
@@ -920,72 +956,129 @@ function CentralRewardSection({
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Produit récompense */}
|
{/* Produits récompense */}
|
||||||
<View>
|
<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 }]}>
|
<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>
|
</Text>
|
||||||
{/* Sélecteur produit */}
|
|
||||||
<View style={{ gap: spacing.s }}>
|
<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 ? (
|
{Object.values(productsByCategory).flat().length === 0 ? (
|
||||||
<Text style={[s.hint, { paddingHorizontal: 0 }]}>Aucun produit disponible</Text>
|
<Text style={[s.hint, { paddingHorizontal: 0 }]}>Aucun produit disponible</Text>
|
||||||
) : (
|
) : (
|
||||||
<View style={{ gap: spacing.xs }}>
|
<TouchableOpacity
|
||||||
<TouchableOpacity
|
onPress={() => update({ reward_items: [...r.reward_items, { product_id: 0, quantity: 1, price: 0 }] })}
|
||||||
onPress={() => update({ reward_product_id: 0, reward_quantity: 0 })}
|
style={{
|
||||||
style={{
|
flexDirection: "row", alignItems: "center", justifyContent: "center", gap: spacing.xs,
|
||||||
flexDirection: "row", alignItems: "center", gap: spacing.xs,
|
paddingHorizontal: spacing.m, paddingVertical: spacing.s,
|
||||||
paddingHorizontal: spacing.m, paddingVertical: spacing.s,
|
borderRadius: borderRadius.sm, borderWidth: 1.5,
|
||||||
borderRadius: borderRadius.sm, borderWidth: 1.5,
|
borderColor: REWARD_ACCENT + "66",
|
||||||
borderColor: !r.reward_product_id ? colors.border : "transparent",
|
}}
|
||||||
backgroundColor: !r.reward_product_id ? colors.border + "30" : "transparent",
|
>
|
||||||
}}
|
<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>
|
||||||
<Text style={{ fontSize: 13, color: !r.reward_product_id ? colors.textPrimary : colors.textMuted }}>
|
</TouchableOpacity>
|
||||||
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>
|
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -1046,7 +1139,7 @@ function CentralRewardSection({
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Récapitulatif */}
|
{/* 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 }}>
|
<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, fontWeight: "700", color: REWARD_ACCENT }}>Récapitulatif</Text>
|
||||||
<Text style={{ fontSize: 13, color: colors.textPrimary }}>
|
<Text style={{ fontSize: 13, color: colors.textPrimary }}>
|
||||||
@@ -1058,14 +1151,22 @@ function CentralRewardSection({
|
|||||||
)}
|
)}
|
||||||
{r.category_configs.map((cfg) => (
|
{r.category_configs.map((cfg) => (
|
||||||
<Text key={cfg.category} style={{ fontSize: 12, color: colors.textSecondary }}>
|
<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>
|
</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>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</AccordionSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1167,7 +1268,7 @@ 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 ?? [], reward_items: s.points_reward.reward_items ?? [] }
|
||||||
: null,
|
: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1397,8 +1498,7 @@ export default function SettingsScreen() {
|
|||||||
<View style={s.container}>
|
<View style={s.container}>
|
||||||
<ScrollView contentContainerStyle={s.content}>
|
<ScrollView contentContainerStyle={s.content}>
|
||||||
{/* Personnalisation */}
|
{/* Personnalisation */}
|
||||||
<View style={s.section}>
|
<AccordionSection title="Personnalisation" colors={colors} s={s}>
|
||||||
<Text style={s.sectionTitle}>Personnalisation</Text>
|
|
||||||
<View style={[s.row, s.rowFirst]}>
|
<View style={[s.row, s.rowFirst]}>
|
||||||
<View style={s.rowLeft}>
|
<View style={s.rowLeft}>
|
||||||
<Text style={s.rowLabel}>Nom du shop</Text>
|
<Text style={s.rowLabel}>Nom du shop</Text>
|
||||||
@@ -1415,11 +1515,10 @@ export default function SettingsScreen() {
|
|||||||
autoCorrect={false}
|
autoCorrect={false}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</AccordionSection>
|
||||||
|
|
||||||
{/* Amendes */}
|
{/* Amendes */}
|
||||||
<View style={s.section}>
|
<AccordionSection title="Amendes" colors={colors} s={s}>
|
||||||
<Text style={s.sectionTitle}>Amendes</Text>
|
|
||||||
<View style={[s.row, s.rowFirst]}>
|
<View style={[s.row, s.rowFirst]}>
|
||||||
<View style={s.rowLeft}>
|
<View style={s.rowLeft}>
|
||||||
<Text style={s.rowLabel}>Amendes activées</Text>
|
<Text style={s.rowLabel}>Amendes activées</Text>
|
||||||
@@ -1522,11 +1621,10 @@ export default function SettingsScreen() {
|
|||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</AccordionSection>
|
||||||
|
|
||||||
{/* Parrainage */}
|
{/* Parrainage */}
|
||||||
<View style={s.section}>
|
<AccordionSection title="Parrainage" colors={colors} s={s}>
|
||||||
<Text style={s.sectionTitle}>Parrainage</Text>
|
|
||||||
<View style={[s.row, s.rowFirst]}>
|
<View style={[s.row, s.rowFirst]}>
|
||||||
<View style={s.rowLeft}>
|
<View style={s.rowLeft}>
|
||||||
<Text style={s.rowLabel}>Parrainage activé</Text>
|
<Text style={s.rowLabel}>Parrainage activé</Text>
|
||||||
@@ -1544,11 +1642,10 @@ export default function SettingsScreen() {
|
|||||||
thumbColor="#fff"
|
thumbColor="#fff"
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</AccordionSection>
|
||||||
|
|
||||||
{/* Système de points */}
|
{/* Système de points */}
|
||||||
<View style={s.section}>
|
<AccordionSection title="Système de points" colors={colors} s={s}>
|
||||||
<Text style={s.sectionTitle}>Système de points</Text>
|
|
||||||
<View style={[s.row, s.rowFirst]}>
|
<View style={[s.row, s.rowFirst]}>
|
||||||
<View style={s.rowLeft}>
|
<View style={s.rowLeft}>
|
||||||
<Text style={s.rowLabel}>Points activés</Text>
|
<Text style={s.rowLabel}>Points activés</Text>
|
||||||
@@ -1604,11 +1701,10 @@ export default function SettingsScreen() {
|
|||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</AccordionSection>
|
||||||
|
|
||||||
{/* Attribution des catégories */}
|
{/* Attribution des catégories */}
|
||||||
<View style={s.section}>
|
<AccordionSection title="Attribution des catégories aux points" colors={colors} s={s}>
|
||||||
<Text style={s.sectionTitle}>Attribution des catégories aux points</Text>
|
|
||||||
<Text style={s.hint}>
|
<Text style={s.hint}>
|
||||||
Choisissez quel type de point est attribué pour chaque catégorie de produit.{"\n"}
|
Choisissez quel type de point est attribué pour chaque catégorie de produit.{"\n"}
|
||||||
Vous pouvez modifier les attributions à tout moment.
|
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.
|
Ajoutez au moins un type de point pour assigner des catégories.
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</AccordionSection>
|
||||||
|
|
||||||
{/* Barèmes de points — un par pool */}
|
{/* Barèmes de points — un par pool */}
|
||||||
{pools.map((pool, i) => (
|
{pools.map((pool, i) => (
|
||||||
@@ -1738,8 +1834,7 @@ export default function SettingsScreen() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Paiement crypto */}
|
{/* Paiement crypto */}
|
||||||
<View style={s.section}>
|
<AccordionSection title="Paiement crypto" colors={colors} s={s}>
|
||||||
<Text style={s.sectionTitle}>Paiement crypto</Text>
|
|
||||||
|
|
||||||
{/* Toggle activation */}
|
{/* Toggle activation */}
|
||||||
<View style={[s.row, s.rowFirst]}>
|
<View style={[s.row, s.rowFirst]}>
|
||||||
@@ -1881,13 +1976,12 @@ export default function SettingsScreen() {
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</AccordionSection>
|
||||||
|
|
||||||
{/* ============================================ */}
|
{/* ============================================ */}
|
||||||
{/* 🤖 TELEGRAM */}
|
{/* 🤖 TELEGRAM */}
|
||||||
{/* ============================================ */}
|
{/* ============================================ */}
|
||||||
<View style={s.section}>
|
<AccordionSection title="Notifications Telegram" colors={colors} s={s}>
|
||||||
<Text style={s.sectionTitle}>Notifications Telegram</Text>
|
|
||||||
<View style={s.row}>
|
<View style={s.row}>
|
||||||
<View style={s.rowLeft}>
|
<View style={s.rowLeft}>
|
||||||
<Text style={s.rowLabel}>Notifications activées</Text>
|
<Text style={s.rowLabel}>Notifications activées</Text>
|
||||||
@@ -1977,13 +2071,12 @@ export default function SettingsScreen() {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</AccordionSection>
|
||||||
|
|
||||||
{/* ============================================ */}
|
{/* ============================================ */}
|
||||||
{/* 🚚 MODE DE LIVRAISON */}
|
{/* 🚚 MODE DE LIVRAISON */}
|
||||||
{/* ============================================ */}
|
{/* ============================================ */}
|
||||||
<View style={s.section}>
|
<AccordionSection title="Mode de livraison" colors={colors} s={s}>
|
||||||
<Text style={s.sectionTitle}>Mode de livraison</Text>
|
|
||||||
<Text style={[s.hint, { paddingTop: spacing.s, paddingHorizontal: spacing.l }]}>
|
<Text style={[s.hint, { paddingTop: spacing.s, paddingHorizontal: spacing.l }]}>
|
||||||
Choisissez comment les commandes sont assignées aux livreurs.
|
Choisissez comment les commandes sont assignées aux livreurs.
|
||||||
</Text>
|
</Text>
|
||||||
@@ -2102,7 +2195,7 @@ export default function SettingsScreen() {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</AccordionSection>
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={s.saveButton}
|
style={s.saveButton}
|
||||||
|
|||||||
Reference in New Issue
Block a user