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.
125 lines
4.6 KiB
TypeScript
125 lines
4.6 KiB
TypeScript
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;
|
||
}
|
||
}
|