Files
Speisekarte/app/api/dishes/import/route.ts
T
florian.wilde cd8b0fab9f feat: Speiseplan-Inhalte variabel mehrsprachig (Teil 2)
Gerichtnamen, Essenszeiten und Allergen-/Zusatzstoff-Bezeichnungen sind
jetzt genauso dynamisch mehrsprachig wie die Bedienoberfläche aus
v1.10.0, statt fest auf Englisch/Polnisch begrenzt zu sein. Neue
Übersetzungstabellen ersetzen die alten name_en/name_pl-Spalten,
DeepL-Auto-Übersetzung und CSV-Import/-Export unterstützen jetzt
beliebig viele aktive Sprachen.
2026-07-20 11:10:06 +02:00

125 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { NextResponse } from "next/server";
import { withTransaction } from "@/lib/db";
import { replaceTranslations } from "@/lib/db/translations";
import { listActiveLanguageCodes } from "@/lib/db/languages";
import { authErrorResponse, requireRole } from "@/lib/auth/session";
import { isSameOrigin } from "@/lib/origin";
import { parseCsvWithHeader, splitCodes } from "@/lib/csv";
interface ImportRow {
rowNum: number;
name: string;
translations: Record<string, string>;
price: number;
allergens: string[];
additives: string[];
}
export async function POST(request: Request) {
if (!isSameOrigin(request)) {
return NextResponse.json({ error: "Nicht erlaubt." }, { status: 403 });
}
try {
await requireRole(["staff", "admin"]);
const form = await request.formData();
const file = form.get("file");
if (!(file instanceof File)) {
return NextResponse.json({ error: "invalid_input" }, { status: 400 });
}
const { header, rows: cells } = parseCsvWithHeader(await file.text());
const nameIdx = header.indexOf("name");
const priceIdx = header.indexOf("price");
if (nameIdx === -1 || priceIdx === -1) {
return NextResponse.json({ error: "missing_columns" }, { status: 400 });
}
const activeCodes = (await listActiveLanguageCodes()).filter((c) => c !== "de");
const langCols = activeCodes
.map((code) => ({ code, idx: header.indexOf(`name_${code}`) }))
.filter((c) => c.idx !== -1);
const allergensIdx = header.indexOf("allergens");
const additivesIdx = header.indexOf("additives");
const rows: ImportRow[] = [];
const skipped: { row: number; reason: string }[] = [];
cells.forEach((c, i) => {
const rowNum = i + 2; // 1-indexiert + Kopfzeile
const name = (c[nameIdx] ?? "").trim();
if (!name) {
skipped.push({ row: rowNum, reason: "Name fehlt" });
return;
}
const priceRaw = (c[priceIdx] ?? "").trim().replace(",", ".");
const price = Number.parseFloat(priceRaw);
if (Number.isNaN(price) || price < 0) {
skipped.push({ row: rowNum, reason: `Ungültiger Preis „${c[priceIdx] ?? ""}“` });
return;
}
const translations: Record<string, string> = {};
for (const { code, idx } of langCols) {
const value = c[idx]?.trim();
if (value) translations[code] = value;
}
rows.push({
rowNum,
name,
translations,
price,
allergens: allergensIdx >= 0 ? splitCodes(c[allergensIdx]) : [],
additives: additivesIdx >= 0 ? splitCodes(c[additivesIdx]) : [],
});
});
let created = 0;
let updated = 0;
const savedTranslations: { id: string; translations: Record<string, string> }[] = [];
if (rows.length > 0) {
await withTransaction(async (client) => {
for (const row of rows) {
// Gerichtsnamen sind nicht eindeutig (keine DB-Constraint) – bei
// mehreren Treffern lieber überspringen als versehentlich alle
// gleichnamigen Gerichte mit denselben Werten zu überschreiben.
const existing = await client.query<{ id: string }>(
"select id from dishes where name = $1",
[row.name]
);
if (existing.rows.length > 1) {
skipped.push({
row: row.rowNum,
reason: `„${row.name}“ ist mehrdeutig (mehrere Gerichte mit diesem Namen) – übersprungen`,
});
continue;
}
if (existing.rows.length === 1) {
await client.query(
`update dishes set price = $1, allergens = $2, additives = $3 where id = $4`,
[row.price, row.allergens, row.additives, existing.rows[0].id]
);
savedTranslations.push({ id: existing.rows[0].id, translations: row.translations });
updated++;
} else {
const inserted = await client.query<{ id: string }>(
`insert into dishes (name, price, allergens, additives)
values ($1, $2, $3, $4) returning id`,
[row.name, row.price, row.allergens, row.additives]
);
savedTranslations.push({ id: inserted.rows[0].id, translations: row.translations });
created++;
}
}
});
for (const { id, translations } of savedTranslations) {
if (Object.keys(translations).length > 0) {
await replaceTranslations("dish_translations", id, translations);
}
}
}
return NextResponse.json({ created, updated, skipped });
} catch (err) {
const res = authErrorResponse(err);
if (res) return res;
throw err;
}
}