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:
@@ -40,6 +40,8 @@ export default function DishManager({
|
||||
const [additives, setAdditives] = useState<CodeItem[]>([]);
|
||||
const [form, setForm] = useState<DishForm | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [notice, setNotice] = useState<string | null>(null);
|
||||
const [ioBusy, setIoBusy] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selected, setSelected] = useState<Set<string>>(new Set());
|
||||
const [bulkBusy, setBulkBusy] = useState(false);
|
||||
@@ -219,6 +221,58 @@ export default function DishManager({
|
||||
}
|
||||
}
|
||||
|
||||
async function exportDishes() {
|
||||
setError(null);
|
||||
setNotice(null);
|
||||
const res = await fetch("/api/dishes/export");
|
||||
if (!res.ok) {
|
||||
setError(formatMessage(dict.dishManager.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 = "gerichte.csv";
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
async function importDishes(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/dishes/import", { method: "POST", body: fd });
|
||||
setIoBusy(false);
|
||||
if (!res.ok) {
|
||||
setError(formatMessage(dict.dishManager.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.dishManager.importSuccess, {
|
||||
created: data.created,
|
||||
updated: data.updated,
|
||||
});
|
||||
if (data.skipped.length > 0) {
|
||||
const reasons = data.skipped
|
||||
.map((s) => formatMessage(dict.dishManager.importSkippedRow, s))
|
||||
.join("; ");
|
||||
msg +=
|
||||
" " + formatMessage(dict.dishManager.importSkipped, { count: data.skipped.length, reasons });
|
||||
}
|
||||
setNotice(msg);
|
||||
await load();
|
||||
}
|
||||
|
||||
const allSelected =
|
||||
visible.length > 0 && visible.every((d) => selected.has(d.id));
|
||||
|
||||
@@ -228,6 +282,19 @@ export default function DishManager({
|
||||
<h1 className="text-xl font-bold text-[var(--c-primary)] mr-auto">
|
||||
{dict.dishManager.title}
|
||||
</h1>
|
||||
<button type="button" className="btn btn-neutral" onClick={exportDishes}>
|
||||
{dict.dishManager.exportButton}
|
||||
</button>
|
||||
<label className="btn btn-neutral cursor-pointer">
|
||||
{dict.dishManager.importButton}
|
||||
<input
|
||||
type="file"
|
||||
accept=".csv,text/csv"
|
||||
className="hidden"
|
||||
onChange={importDishes}
|
||||
disabled={ioBusy}
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-add"
|
||||
@@ -245,6 +312,7 @@ export default function DishManager({
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
{notice && <p className="mb-3 text-sm text-green-700">{notice}</p>}
|
||||
|
||||
{form && (
|
||||
<form onSubmit={save} className="card mb-4 space-y-4">
|
||||
|
||||
Reference in New Issue
Block a user