chore: update

This commit is contained in:
2026-03-08 22:34:54 +01:00
parent 20e9918941
commit 0a144cd07b
7 changed files with 265 additions and 42 deletions
+13
View File
@@ -704,6 +704,12 @@ export interface PointsTier {
points: number;
}
export interface PostalZone {
name: string;
min_amount: number;
codes: string[];
}
export interface DaySchedule {
enabled: boolean;
open_time: string; // "09:00"
@@ -720,6 +726,12 @@ export interface DeliverySchedule {
sunday: DaySchedule;
}
export const DEFAULT_POSTAL_ZONES: PostalZone[] = [
{ name: "Zone 30€", min_amount: 30, codes: ["44000", "44100", "44200", "44300"] },
{ name: "Zone 50€", min_amount: 50, codes: ["44400", "44880", "44120", "44230", "44115", "44980", "44470", "44240", "44700", "44800", "44340", "44620", "44830"] },
{ name: "Zone 100€", min_amount: 100, codes: ["44860", "44220", "44118", "44710", "44690", "44119"] },
];
export const DEFAULT_DAY: DaySchedule = { enabled: true, open_time: "09:00", close_time: "20:00" };
export const DEFAULT_DELIVERY_SCHEDULE: DeliverySchedule = {
@@ -741,6 +753,7 @@ export interface AppSettings {
points_total_tiers: PointsTier[];
referral_enabled: boolean;
delivery_schedule: DeliverySchedule;
postal_zones: PostalZone[];
}
export const getSettings = async (): Promise<{
@@ -12,8 +12,8 @@ import {
import { Ionicons } from "@expo/vector-icons";
import { spacing, fontSize, borderRadius } from "../../theme";
import { useTheme } from "../../context/ThemeContext";
import { getSettings, updateSettings, getCategories, DEFAULT_DELIVERY_SCHEDULE } from "../../api/api_admin";
import type { AppSettings, Category, PointsTier, DaySchedule, DeliverySchedule } from "../../api/api_admin";
import { getSettings, updateSettings, getCategories, DEFAULT_DELIVERY_SCHEDULE, DEFAULT_POSTAL_ZONES } from "../../api/api_admin";
import type { AppSettings, Category, PointsTier, DaySchedule, DeliverySchedule, PostalZone } from "../../api/api_admin";
import AlertModal from "../../components/ui/AlertModal";
import { useAlert } from "../../hooks/useAlert";
@@ -119,6 +119,226 @@ function DeliveryScheduleSection({
);
}
// ──────────────────────────────────────────────────────────────
// Composant éditeur de zones postales
// ──────────────────────────────────────────────────────────────
function PostalZonesSection({
zones,
onChange,
colors,
s,
}: {
zones: PostalZone[];
onChange: (zones: PostalZone[]) => void;
colors: any;
s: any;
}) {
const [expandedIndex, setExpandedIndex] = React.useState<number | null>(null);
const addZone = () => {
const next = [...zones, { name: "Nouvelle zone", min_amount: 0, codes: [] }];
onChange(next);
setExpandedIndex(next.length - 1);
};
const removeZone = (i: number) => {
onChange(zones.filter((_, idx) => idx !== i));
if (expandedIndex === i) setExpandedIndex(null);
};
const updateZone = (i: number, patch: Partial<PostalZone>) => {
onChange(zones.map((z, idx) => (idx === i ? { ...z, ...patch } : z)));
};
const addCode = (i: number, code: string) => {
const trimmed = code.trim();
if (!trimmed || zones[i].codes.includes(trimmed)) return;
updateZone(i, { codes: [...zones[i].codes, trimmed] });
};
const removeCode = (zoneIdx: number, codeIdx: number) => {
updateZone(zoneIdx, {
codes: zones[zoneIdx].codes.filter((_, idx) => idx !== codeIdx),
});
};
return (
<View style={s.section}>
<Text style={s.sectionTitle}>Zones de livraison</Text>
<Text style={[s.hint, { paddingTop: spacing.s }]}>
Définissez les zones de livraison et leur montant minimum de commande.
</Text>
{zones.map((zone, i) => {
const expanded = expandedIndex === i;
return (
<View key={i} style={{ borderTopWidth: 1, borderTopColor: colors.border }}>
{/* En-tête zone */}
<TouchableOpacity
onPress={() => setExpandedIndex(expanded ? null : i)}
style={{
flexDirection: "row",
alignItems: "center",
paddingHorizontal: spacing.l,
paddingVertical: spacing.m,
gap: spacing.s,
}}
activeOpacity={0.7}
>
<Ionicons
name={expanded ? "chevron-down" : "chevron-forward"}
size={16}
color={colors.textMuted}
/>
<Text style={[s.rowLabel, { flex: 1 }]} numberOfLines={1}>
{zone.name || "Zone sans nom"}
</Text>
<Text style={{ fontSize: fontSize.sm, color: colors.accent, fontWeight: "700" }}>
{zone.min_amount > 0 ? `${zone.min_amount}€ min` : "Sans min"}
</Text>
<Text style={{ fontSize: fontSize.xs, color: colors.textMuted, marginLeft: spacing.xs }}>
{zone.codes.length} codes
</Text>
<TouchableOpacity onPress={() => removeZone(i)} style={{ marginLeft: spacing.s }}>
<Ionicons name="trash-outline" size={18} color={colors.danger} />
</TouchableOpacity>
</TouchableOpacity>
{/* Détail zone */}
{expanded && (
<View style={{ paddingHorizontal: spacing.l, paddingBottom: spacing.m, gap: spacing.m }}>
{/* Nom */}
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.s }}>
<Text style={[s.thresholdSep, { width: 48 }]}>Nom</Text>
<TextInput
style={[s.thresholdInput, { flex: 1, width: undefined, textAlign: "left", paddingHorizontal: spacing.m }]}
value={zone.name}
onChangeText={(v) => updateZone(i, { name: v })}
placeholder="Ex: Zone centre-ville"
placeholderTextColor={colors.textMuted}
/>
</View>
{/* Minimum */}
<View style={{ flexDirection: "row", alignItems: "center", gap: spacing.s }}>
<Text style={[s.thresholdSep, { width: 48 }]}>Min €</Text>
<TextInput
style={[s.thresholdInput, { width: 80 }]}
value={String(zone.min_amount)}
onChangeText={(v) => {
const n = parseFloat(v);
if (!isNaN(n)) updateZone(i, { min_amount: n });
}}
keyboardType="decimal-pad"
placeholder="0"
placeholderTextColor={colors.textMuted}
/>
</View>
{/* Codes postaux */}
<Text style={[s.thresholdSep, { marginBottom: 0 }]}>Codes postaux :</Text>
<View style={{ flexDirection: "row", flexWrap: "wrap", gap: spacing.xs }}>
{zone.codes.map((code, ci) => (
<TouchableOpacity
key={ci}
onPress={() => removeCode(i, ci)}
style={{
flexDirection: "row",
alignItems: "center",
backgroundColor: colors.accent + "20",
borderRadius: borderRadius.sm,
paddingHorizontal: spacing.s,
paddingVertical: 4,
gap: 4,
}}
>
<Text style={{ fontSize: fontSize.sm, color: colors.accent, fontWeight: "600" }}>
{code}
</Text>
<Ionicons name="close" size={12} color={colors.accent} />
</TouchableOpacity>
))}
<PostalCodeInput
onAdd={(code) => addCode(i, code)}
colors={colors}
s={s}
/>
</View>
</View>
)}
</View>
);
})}
{zones.length === 0 && (
<Text style={[s.hint, { paddingTop: 0 }]}>Aucune zone définie — toutes les commandes seront acceptées sans minimum.</Text>
)}
<TouchableOpacity
onPress={addZone}
style={{
flexDirection: "row",
alignItems: "center",
gap: spacing.xs,
paddingHorizontal: spacing.l,
paddingVertical: spacing.m,
borderTopWidth: 1,
borderTopColor: colors.border,
}}
>
<Ionicons name="add-circle-outline" size={18} color={colors.accent} />
<Text style={{ fontSize: fontSize.sm, color: colors.accent, fontWeight: "600" }}>
Ajouter une zone
</Text>
</TouchableOpacity>
</View>
);
}
function PostalCodeInput({
onAdd,
colors,
s,
}: {
onAdd: (code: string) => void;
colors: any;
s: any;
}) {
const [value, setValue] = React.useState("");
return (
<View style={{ flexDirection: "row", alignItems: "center", gap: 4 }}>
<TextInput
style={[s.thresholdInput, { width: 72, fontSize: fontSize.sm }]}
value={value}
onChangeText={setValue}
placeholder="44XXX"
placeholderTextColor={colors.textMuted}
keyboardType="number-pad"
maxLength={5}
onSubmitEditing={() => {
if (value.length === 5) {
onAdd(value);
setValue("");
}
}}
/>
<TouchableOpacity
onPress={() => {
if (value.length === 5) {
onAdd(value);
setValue("");
}
}}
style={{
backgroundColor: colors.accent,
borderRadius: borderRadius.sm,
padding: 6,
}}
>
<Ionicons name="add" size={14} color="#fff" />
</TouchableOpacity>
</View>
);
}
// ──────────────────────────────────────────────────────────────
// Composant éditeur de paliers
// ──────────────────────────────────────────────────────────────
@@ -270,6 +490,7 @@ export default function SettingsScreen() {
points_total_tiers: [],
referral_enabled: true,
delivery_schedule: DEFAULT_DELIVERY_SCHEDULE,
postal_zones: DEFAULT_POSTAL_ZONES,
});
const [categories, setCategories] = useState<Category[]>([]);
@@ -287,6 +508,7 @@ export default function SettingsScreen() {
points_categories_total: settingsRes.settings.points_categories_total ?? [],
points_total_tiers: settingsRes.settings.points_total_tiers ?? [],
delivery_schedule: settingsRes.settings.delivery_schedule ?? DEFAULT_DELIVERY_SCHEDULE,
postal_zones: settingsRes.settings.postal_zones ?? DEFAULT_POSTAL_ZONES,
});
}
if (categoriesRes) {
@@ -683,6 +905,14 @@ export default function SettingsScreen() {
s={s}
/>
{/* Zones de livraison */}
<PostalZonesSection
zones={settings.postal_zones ?? DEFAULT_POSTAL_ZONES}
onChange={(zones) => setSettings((p) => ({ ...p, postal_zones: zones }))}
colors={colors}
s={s}
/>
<TouchableOpacity
style={s.saveButton}
onPress={handleSave}