// 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 (