feat: CSV-Import/-Export für Gerichte/Allergene/Zusatzstoffe, RSS-Fix für mehrere Essenszeiten
CSV-Import/-Export (Gerichte, Allergene, Zusatzstoffe): neue Export-/ Import-Routen unter app/api/*/export und */import, hand-geschriebener CSV-Parser/-Serializer (lib/csv.ts, keine neue Abhängigkeit), Upsert über Kürzel (Allergene/Zusatzstoffe) bzw. exakten Namen (Gerichte, mit Ambiguitäts-Check bei Namensdopplung). UI-Buttons in DishManager und CodeListManager. RSS-Feed: <content:encoded> zusätzlich zu <description> ausgeben, da mehrere Feed-Reader description als kürzbare Vorschau behandeln und dadurch bei mehreren Essenszeiten pro Tag nur die erste zeigten. Version 1.7.0. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,8 @@ export default function CodeListManager({
|
||||
namePl: string;
|
||||
} | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [notice, setNotice] = useState<string | null>(null);
|
||||
const [ioBusy, setIoBusy] = useState(false);
|
||||
const [translateState, setTranslateState] = useState<"idle" | "busy" | "done">(
|
||||
"idle"
|
||||
);
|
||||
@@ -125,12 +127,77 @@ export default function CodeListManager({
|
||||
}
|
||||
}
|
||||
|
||||
async function exportItems() {
|
||||
setError(null);
|
||||
setNotice(null);
|
||||
const res = await fetch(`/api/${table}/export`);
|
||||
if (!res.ok) {
|
||||
setError(formatMessage(dict.codeList.exportError, { message: String(res.status) }));
|
||||
return;
|
||||
}
|
||||
const blob = await res.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = table === "allergens" ? "allergene.csv" : "zusatzstoffe.csv";
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
async function importItems(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
const file = e.target.files?.[0];
|
||||
e.target.value = "";
|
||||
if (!file) return;
|
||||
setError(null);
|
||||
setNotice(null);
|
||||
setIoBusy(true);
|
||||
const fd = new FormData();
|
||||
fd.append("file", file);
|
||||
const res = await fetch(`/api/${table}/import`, { method: "POST", body: fd });
|
||||
setIoBusy(false);
|
||||
if (!res.ok) {
|
||||
setError(formatMessage(dict.codeList.importError, { message: String(res.status) }));
|
||||
return;
|
||||
}
|
||||
const data = (await res.json()) as {
|
||||
created: number;
|
||||
updated: number;
|
||||
skipped: { row: number; reason: string }[];
|
||||
};
|
||||
let msg = formatMessage(dict.codeList.importSuccess, {
|
||||
created: data.created,
|
||||
updated: data.updated,
|
||||
});
|
||||
if (data.skipped.length > 0) {
|
||||
const reasons = data.skipped
|
||||
.map((s) => formatMessage(dict.codeList.importSkippedRow, s))
|
||||
.join("; ");
|
||||
msg +=
|
||||
" " + formatMessage(dict.codeList.importSkipped, { count: data.skipped.length, reasons });
|
||||
}
|
||||
setNotice(msg);
|
||||
await load();
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="card">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<h2 className="text-lg font-bold text-[var(--c-primary)] mr-auto">
|
||||
{title}
|
||||
</h2>
|
||||
<button type="button" className="btn btn-neutral" onClick={exportItems}>
|
||||
{dict.codeList.exportButton}
|
||||
</button>
|
||||
<label className="btn btn-neutral cursor-pointer">
|
||||
{dict.codeList.importButton}
|
||||
<input
|
||||
type="file"
|
||||
accept=".csv,text/csv"
|
||||
className="hidden"
|
||||
onChange={importItems}
|
||||
disabled={ioBusy}
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-add"
|
||||
@@ -148,6 +215,7 @@ export default function CodeListManager({
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
{notice && <p className="mb-2 text-sm text-green-700">{notice}</p>}
|
||||
|
||||
{editing && (
|
||||
<form onSubmit={save} className="mb-3 space-y-2 border-b border-gray-200 pb-3">
|
||||
|
||||
Reference in New Issue
Block a user