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>
86 lines
3.0 KiB
TypeScript
86 lines
3.0 KiB
TypeScript
import "server-only";
|
||
import { query, queryOne, withTransaction } from "@/lib/db";
|
||
import type { CodeItem } from "@/lib/types";
|
||
|
||
// "allergens"/"additives" sind identisch aufgebaute Tabellen (Kürzel +
|
||
// Bezeichnung in DE/EN/PL). Tabellenname kommt nie aus Nutzereingaben,
|
||
// sondern immer aus dem literal getypten Parameter unten – sicher für
|
||
// String-Interpolation in der Query.
|
||
export type CodeTable = "allergens" | "additives";
|
||
|
||
export async function listCodes(table: CodeTable): Promise<CodeItem[]> {
|
||
return query<CodeItem>(`select * from ${table} order by sort`);
|
||
}
|
||
|
||
export async function createCode(
|
||
table: CodeTable,
|
||
input: { code: string; name: string; name_en: string | null; name_pl: string | null }
|
||
): Promise<CodeItem> {
|
||
const maxRow = await queryOne<{ sort: number }>(
|
||
`select sort from ${table} order by sort desc limit 1`
|
||
);
|
||
return (await queryOne<CodeItem>(
|
||
`insert into ${table} (code, name, name_en, name_pl, sort) values ($1, $2, $3, $4, $5) returning *`,
|
||
[input.code, input.name, input.name_en, input.name_pl, (maxRow?.sort ?? 0) + 1]
|
||
))!;
|
||
}
|
||
|
||
export async function updateCode(
|
||
table: CodeTable,
|
||
originalCode: string,
|
||
input: { code: string; name: string; name_en: string | null; name_pl: string | null }
|
||
): Promise<CodeItem | null> {
|
||
return queryOne<CodeItem>(
|
||
`update ${table} set code = $1, name = $2, name_en = $3, name_pl = $4 where code = $5 returning *`,
|
||
[input.code, input.name, input.name_en, input.name_pl, originalCode]
|
||
);
|
||
}
|
||
|
||
export async function deleteCode(table: CodeTable, code: string): Promise<void> {
|
||
await query(`delete from ${table} where code = $1`, [code]);
|
||
}
|
||
|
||
export interface CodeImportRow {
|
||
code: string;
|
||
name: string;
|
||
name_en: string | null;
|
||
name_pl: string | null;
|
||
}
|
||
|
||
/** CSV-Import: legt neue Kürzel an, bestehende (gleicher Code) werden
|
||
* aktualisiert – ein doppelter Code innerhalb derselben Datei
|
||
* aktualisiert also einfach zweimal statt einen Fehler zu werfen. */
|
||
export async function importCodes(
|
||
table: CodeTable,
|
||
rows: CodeImportRow[]
|
||
): Promise<{ created: number; updated: number }> {
|
||
return withTransaction(async (client) => {
|
||
const existing = await client.query<{ code: string }>(`select code from ${table}`);
|
||
const known = new Set(existing.rows.map((r) => r.code));
|
||
const maxRow = await client.query<{ sort: number }>(
|
||
`select coalesce(max(sort), 0) as sort from ${table}`
|
||
);
|
||
let nextSort = (maxRow.rows[0]?.sort ?? 0) + 1;
|
||
let created = 0;
|
||
let updated = 0;
|
||
|
||
for (const row of rows) {
|
||
if (known.has(row.code)) {
|
||
await client.query(
|
||
`update ${table} set name = $1, name_en = $2, name_pl = $3 where code = $4`,
|
||
[row.name, row.name_en, row.name_pl, row.code]
|
||
);
|
||
updated++;
|
||
} else {
|
||
await client.query(
|
||
`insert into ${table} (code, name, name_en, name_pl, sort) values ($1, $2, $3, $4, $5)`,
|
||
[row.code, row.name, row.name_en, row.name_pl, nextSort++]
|
||
);
|
||
known.add(row.code);
|
||
created++;
|
||
}
|
||
}
|
||
return { created, updated };
|
||
});
|
||
}
|