// Composants UI reutilisables — palette warm artisanal // Inspires du dashboard atelier (memes polices, memes couleurs, meme tonalite) // ============================================================ // Hooks utilitaires // ============================================================ function useAsyncData(loader, deps = []) { const [data, setData] = React.useState(null); const [error, setError] = React.useState(null); const [loading, setLoading] = React.useState(true); const reload = React.useCallback(() => { setLoading(true); loader() .then((d) => { setData(d); setError(null); }) .catch((e) => setError(e.message || String(e))) .finally(() => setLoading(false)); }, deps); React.useEffect(() => { reload(); }, [reload]); return { data, error, loading, reload, setData }; } // Toast "Sauvegarde" affiche brievement apres une PUT/POST const ToastContext = React.createContext({ showToast: () => {} }); function ToastProvider({ children }) { const [toasts, setToasts] = React.useState([]); const showToast = (msg, kind = "ok") => { const id = Date.now() + Math.random(); setToasts((prev) => [...prev, { id, msg, kind }]); setTimeout(() => setToasts((prev) => prev.filter((t) => t.id !== id)), 1700); }; return ( {children}
{toasts.map((t) => (
{t.msg}
))}
); } const useToast = () => React.useContext(ToastContext); // ============================================================ // Sector badge (Pain / Viennoiserie) // ============================================================ function SectorBadge({ secteur }) { const isPain = secteur === "pain"; return ( {isPain ? "Pain" : "Viennoiserie"} ); } // ============================================================ // Statut complet/incomplet // ============================================================ function StatutBadge({ statut, size = "sm" }) { const ok = statut === "complet"; const cls = size === "sm" ? "text-xs px-2 py-0.5" : "text-sm px-2.5 py-1"; return ( {ok ? "Complet" : "À compléter"} ); } // ============================================================ // Champ editable inline (texte / nombre) // Auto-save sur blur — appelle onSave(newValue) // ============================================================ function EditableField({ value, onSave, type = "text", placeholder = "—", className = "", numeric = false, inputMode }) { const [val, setVal] = React.useState(value ?? ""); const [saving, setSaving] = React.useState(false); React.useEffect(() => { setVal(value ?? ""); }, [value]); const handleBlur = async () => { const current = val; const before = value ?? ""; if (String(current) === String(before)) return; setSaving(true); try { let toSend = current === "" ? null : current; if (numeric && toSend !== null) { const n = Number(String(toSend).replace(",", ".")); toSend = isNaN(n) ? null : n; } await onSave(toSend); } catch (e) { setVal(value ?? ""); } finally { setSaving(false); } }; return ( setVal(e.target.value)} onBlur={handleBlur} disabled={saving} className={`field-edit ${numeric ? "font-mono tnum" : ""} ${className}`} /> ); } function EditableTextarea({ value, onSave, placeholder = "", className = "", rows = 8 }) { const [val, setVal] = React.useState(value ?? ""); const [saving, setSaving] = React.useState(false); React.useEffect(() => { setVal(value ?? ""); }, [value]); const handleBlur = async () => { if ((val ?? "") === (value ?? "")) return; setSaving(true); try { await onSave(val === "" ? null : val); } catch (e) { setVal(value ?? ""); } finally { setSaving(false); } }; return (