Files
Speisekarte/lib/db/code-lists.ts
T
florian.wildeandClaude Sonnet 5 474467811c 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>
2026-07-19 23:10:36 +02:00

86 lines
3.0 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 "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 };
});
}