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; 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 = {}; 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 }[] = []; 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; } }