diff --git a/frontend-admin/src/screens/admin/SettingsScreen.tsx b/frontend-admin/src/screens/admin/SettingsScreen.tsx index df4ab72f..3705626e 100644 --- a/frontend-admin/src/screens/admin/SettingsScreen.tsx +++ b/frontend-admin/src/screens/admin/SettingsScreen.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from "react"; +import React, { useState, useEffect, useCallback, useRef } from "react"; import { View, Text, @@ -8,7 +8,9 @@ import { TouchableOpacity, ActivityIndicator, TextInput, + Keyboard, } from "react-native"; +import { useNavigation } from "@react-navigation/native"; import { Ionicons } from "@expo/vector-icons"; import { spacing, fontSize, borderRadius } from "../../theme"; import { useTheme } from "../../context/ThemeContext"; @@ -122,6 +124,17 @@ function DeliveryScheduleSection({ // ────────────────────────────────────────────────────────────── // Composant éditeur de zones postales // ────────────────────────────────────────────────────────────── + +function parsePostalCodes(raw: string): string[] { + return raw + .split(/[\s,;]+/) + .map((c) => c.trim()) + .filter((c) => /^\d{5}$/.test(c)); +} + +type NewZoneForm = { name: string; min_amount: string; codesRaw: string }; +const EMPTY_FORM: NewZoneForm = { name: "", min_amount: "", codesRaw: "" }; + function PostalZonesSection({ zones, onChange, @@ -134,12 +147,9 @@ function PostalZonesSection({ s: any; }) { const [expandedIndex, setExpandedIndex] = React.useState(null); - - const addZone = () => { - const next = [...zones, { name: "Nouvelle zone", min_amount: 0, codes: [] }]; - onChange(next); - setExpandedIndex(next.length - 1); - }; + const [creating, setCreating] = React.useState(false); + const [form, setForm] = React.useState(EMPTY_FORM); + const [formError, setFormError] = React.useState(""); const removeZone = (i: number) => { onChange(zones.filter((_, idx) => idx !== i)); @@ -150,18 +160,26 @@ function PostalZonesSection({ 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), }); }; + const confirmCreate = () => { + const name = form.name.trim(); + if (!name) { setFormError("Le nom de la zone est obligatoire."); return; } + const min = parseFloat(form.min_amount); + if (isNaN(min) || min < 0) { setFormError("Montant minimum invalide."); return; } + const codes = parsePostalCodes(form.codesRaw); + if (codes.length === 0) { setFormError("Ajoutez au moins un code postal valide (5 chiffres)."); return; } + onChange([...zones, { name, min_amount: min, codes }]); + setCreating(false); + setForm(EMPTY_FORM); + setFormError(""); + setExpandedIndex(zones.length); // expand newly created + }; + return ( Zones de livraison @@ -204,7 +222,7 @@ function PostalZonesSection({ - {/* Détail zone */} + {/* Détail zone (édition) */} {expanded && ( {/* Nom */} @@ -233,8 +251,10 @@ function PostalZonesSection({ placeholderTextColor={colors.textMuted} /> - {/* Codes postaux */} - Codes postaux : + {/* Codes postaux — chips supprimables */} + + Codes postaux ({zone.codes.length}) — appuyer pour supprimer : + {zone.codes.map((code, ci) => ( ))} - addCode(i, code)} - colors={colors} - s={s} - /> + {/* Ajout en masse */} + { + const existing = new Set(zone.codes); + const toAdd = newCodes.filter((c) => !existing.has(c)); + if (toAdd.length > 0) updateZone(i, { codes: [...zone.codes, ...toAdd] }); + }} + colors={colors} + s={s} + /> )} ); })} - {zones.length === 0 && ( + {zones.length === 0 && !creating && ( Aucune zone définie — toutes les commandes seront acceptées sans minimum. )} - - - - Ajouter une zone - - + {/* Formulaire de création */} + {creating && ( + + Nouvelle zone + + {/* Nom */} + + Nom de la zone * + { setForm((f) => ({ ...f, name: v })); setFormError(""); }} + placeholder="Ex: Zone centre-ville" + placeholderTextColor={colors.textMuted} + autoFocus + /> + + + {/* Minimum */} + + Montant minimum de commande (€) * + { setForm((f) => ({ ...f, min_amount: v })); setFormError(""); }} + keyboardType="decimal-pad" + placeholder="Ex: 30" + placeholderTextColor={colors.textMuted} + /> + + + {/* Codes postaux */} + + Codes postaux * (séparés par virgules ou espaces) + { setForm((f) => ({ ...f, codesRaw: v })); setFormError(""); }} + placeholder={"44000, 44100, 44200\n44300 44400"} + placeholderTextColor={colors.textMuted} + multiline + keyboardType="numbers-and-punctuation" + /> + {form.codesRaw.length > 0 && ( + + {parsePostalCodes(form.codesRaw).length} code(s) valide(s) détecté(s) + + )} + + + {formError !== "" && ( + + {formError} + + )} + + + + + Créer la zone + + { setCreating(false); setForm(EMPTY_FORM); setFormError(""); }} + style={{ + paddingHorizontal: spacing.l, + borderRadius: borderRadius.sm, + paddingVertical: spacing.s, + alignItems: "center", + justifyContent: "center", + borderWidth: 1, + borderColor: colors.border, + }} + > + Annuler + + + + )} + + {!creating && ( + { setCreating(true); setForm(EMPTY_FORM); setFormError(""); }} + style={{ + flexDirection: "row", + alignItems: "center", + gap: spacing.xs, + paddingHorizontal: spacing.l, + paddingVertical: spacing.m, + borderTopWidth: 1, + borderTopColor: colors.border, + }} + > + + + Ajouter une zone + + + )} ); } -function PostalCodeInput({ +function BulkCodesInput({ onAdd, colors, s, }: { - onAdd: (code: string) => void; + onAdd: (codes: string[]) => void; colors: any; s: any; }) { const [value, setValue] = React.useState(""); + const parsed = parsePostalCodes(value); + + const handleAdd = () => { + if (parsed.length === 0) return; + onAdd(parsed); + setValue(""); + }; + return ( - - { - if (value.length === 5) { - onAdd(value); - setValue(""); - } - }} - /> - { - if (value.length === 5) { - onAdd(value); - setValue(""); - } - }} - style={{ - backgroundColor: colors.accent, - borderRadius: borderRadius.sm, - padding: 6, - }} - > - - + + Ajouter des codes (virgules ou espaces) : + + + 0 ? colors.accent : colors.border, + borderRadius: borderRadius.sm, + padding: spacing.s, + alignItems: "center", + justifyContent: "center", + minWidth: 48, + height: 60, + }} + disabled={parsed.length === 0} + > + + {parsed.length > 0 && ( + {parsed.length} + )} + + + {value.length > 0 && parsed.length > 0 && ( + + {parsed.length} code(s) valide(s) : {parsed.join(", ")} + + )} ); } @@ -474,6 +626,7 @@ function TiersSection({ export default function SettingsScreen() { const { colors } = useTheme(); const { alert, showError, showSuccess, hideAlert } = useAlert(); + const navigation = useNavigation(); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); @@ -494,8 +647,33 @@ export default function SettingsScreen() { }); const [categories, setCategories] = useState([]); + // Refs pour l'auto-sauvegarde au départ de la page + const settingsRef = useRef(settings); + const isDirty = useRef(false); + const isLoaded = useRef(false); + + useEffect(() => { + settingsRef.current = settings; + if (isLoaded.current) { + isDirty.current = true; + } + }, [settings]); + + // Auto-sauvegarde silencieuse quand l'utilisateur quitte la page + useEffect(() => { + const unsubscribe = navigation.addListener("beforeRemove", () => { + if (isDirty.current) { + Keyboard.dismiss(); + updateSettings(settingsRef.current).catch(() => {}); + isDirty.current = false; + } + }); + return unsubscribe; + }, [navigation]); + const loadData = useCallback(async () => { setLoading(true); + isLoaded.current = false; const [settingsRes, categoriesRes] = await Promise.all([ getSettings(), getCategories(), @@ -515,6 +693,9 @@ export default function SettingsScreen() { setCategories(categoriesRes.filter((c) => !c.is_coming_soon)); } setLoading(false); + // Marquer comme chargé après un tick pour que le useEffect de settings ne + // considère pas le setSettings initial comme une modification utilisateur + setTimeout(() => { isLoaded.current = true; isDirty.current = false; }, 0); }, []); useEffect(() => { @@ -541,10 +722,15 @@ export default function SettingsScreen() { }; const handleSave = async () => { + // Fermer le clavier pour s'assurer que les TextInputs commitent leur valeur + Keyboard.dismiss(); setSaving(true); - const res = await updateSettings(settings); + // Utiliser le ref pour avoir la valeur la plus à jour (post-blur) + const current = settingsRef.current; + const res = await updateSettings(current); setSaving(false); if (res.success) { + isDirty.current = false; showSuccess("Succès", "Paramètres sauvegardés"); } else { showError("Erreur", res.error || "Erreur lors de la sauvegarde");