Verwaltung: QR-Typen einzeln freigeben oder ausblenden

Neue Seite Verwaltung -> QR-Typen mit einem Schalter je Typ. Abgeschaltete
Typen verschwinden vollstaendig von der Startseite und lassen sich nicht
mehr neu anlegen.

- Neue Tabelle QrTypeSetting; fehlender Eintrag heisst freigegeben, damit
  eine frische Installation ohne Vorbelegung laeuft und spaetere Typen
  automatisch erscheinen.
- Registry-Feld 'active' heisst jetzt 'implemented' und beschreibt nur noch,
  ob der Typ ausprogrammiert ist. Ob er erscheint, entscheidet zusaetzlich
  die Freigabe aus der Datenbank (src/lib/qr/availability.ts).
- Bestehende dynamische Codes bleiben von einer Abschaltung unberuehrt: die
  Weiterleitung funktioniert weiter und Bearbeiten bleibt moeglich. Ein
  gedruckter Code darf durch einen Schalter nicht ungueltig werden.
- resolveQrRequest ist dadurch asynchron geworden; QrRequestError liegt jetzt
  in type-module.ts, sonst gaebe es einen Ringschluss mit availability.ts.
- Startseite meldet jetzt auch den Fall, dass nur noch Platzhalter
  freigegeben sind - vorher standen dort 14 tote Kacheln ohne Erklaerung.
- Kopfzeile: Schriftzug auf schmalen Geraeten ausgeblendet, sonst passte die
  dritte Navigationsschaltflaeche nicht mehr in die Zeile (7px Ueberlauf).
This commit is contained in:
Florian Wilde
2026-08-14 10:48:21 +02:00
parent 1e9e37f08d
commit c6da20f419
18 changed files with 684 additions and 148 deletions
+51
View File
@@ -0,0 +1,51 @@
import type { NextRequest } from "next/server";
import { z } from "zod";
import { errorResponse, firstZodMessage, jsonError } from "@/lib/http";
import {
getQrTypeAvailability,
setQrTypeEnabled,
} from "@/lib/qr/availability";
import { QR_TYPE_IDS } from "@/lib/qr/type-module";
export const dynamic = "force-dynamic";
const updateSchema = z.object({
type: z.enum(QR_TYPE_IDS, { error: "Unbekannter QR-Typ." }),
enabled: z.boolean({ error: "Der Schalter muss wahr oder falsch sein." }),
});
/** Zustand aller QR-Typen für die Verwaltung. */
export async function GET(): Promise<Response> {
try {
const types = await getQrTypeAvailability();
return Response.json({
types: types.map(({ qrType, implemented, enabled, usable }) => ({
id: qrType.id,
title: qrType.title,
description: qrType.description,
icon: qrType.icon,
implemented,
enabled,
usable,
})),
});
} catch (error) {
return errorResponse(error);
}
}
/** Einen Typ freigeben oder abschalten. */
export async function PATCH(request: NextRequest): Promise<Response> {
try {
const parsed = updateSchema.safeParse(await request.json());
if (!parsed.success) {
return jsonError(firstZodMessage(parsed.error));
}
await setQrTypeEnabled(parsed.data.type, parsed.data.enabled);
return Response.json({ type: parsed.data.type, enabled: parsed.data.enabled });
} catch (error) {
return errorResponse(error);
}
}
+6 -8
View File
@@ -5,9 +5,9 @@ import type { Prisma } from "@/generated/prisma/client";
import { toJsonInput, toQrCodeDto } from "@/lib/codes";
import { prisma } from "@/lib/db";
import { errorResponse, firstZodMessage, jsonError } from "@/lib/http";
import { requireImplementedQrType } from "@/lib/qr/availability";
import { designSchema } from "@/lib/qr/design";
import { getActiveQrType } from "@/lib/qr/registry";
import { QrRequestError } from "@/lib/qr/request";
import { QrRequestError } from "@/lib/qr/type-module";
export const dynamic = "force-dynamic";
@@ -60,12 +60,10 @@ export async function PATCH(
const update: Prisma.QrCodeUpdateInput = {};
if (parsed.data.data !== undefined) {
const qrType = getActiveQrType(existing.type);
if (!qrType) {
throw new QrRequestError(
"Der Typ dieses Codes wird in dieser Version nicht unterstützt.",
);
}
// Bewusst nur „ausprogrammiert“, nicht „freigegeben“: ein in der
// Verwaltung abgeschalteter Typ soll bestehende Codes nicht
// unbearbeitbar machen — sie sind längst gedruckt.
const qrType = requireImplementedQrType(existing.type);
const validated = qrType.validate(parsed.data.data);
if (!validated.ok) {
return jsonError(validated.message);
+3 -10
View File
@@ -4,10 +4,9 @@ import { z } from "zod";
import { toJsonInput, toQrCodeDto } from "@/lib/codes";
import { prisma } from "@/lib/db";
import { errorResponse, firstZodMessage, jsonError } from "@/lib/http";
import { requireAvailableQrType } from "@/lib/qr/availability";
import { designSchema } from "@/lib/qr/design";
import { getActiveQrType } from "@/lib/qr/registry";
import { QrRequestError } from "@/lib/qr/request";
import { QR_TYPE_IDS } from "@/lib/qr/type-module";
import { QR_TYPE_IDS, QrRequestError } from "@/lib/qr/type-module";
import { generateSlug } from "@/lib/slug";
export const dynamic = "force-dynamic";
@@ -51,13 +50,7 @@ export async function POST(request: NextRequest): Promise<Response> {
}
const { type, design, label } = parsed.data;
const qrType = getActiveQrType(type);
if (!qrType) {
throw new QrRequestError(
"Dieser QR-Typ ist in dieser Version noch nicht verfügbar.",
404,
);
}
const qrType = await requireAvailableQrType(type);
if (!qrType.supportsDynamic) {
throw new QrRequestError(
`Der QR-Typ „${qrType.title}“ unterstützt keine dynamischen Codes.`,
+1 -1
View File
@@ -31,7 +31,7 @@ export async function POST(request: NextRequest): Promise<Response> {
);
}
const { qrType, data, content } = resolveQrRequest(parsed.data);
const { qrType, data, content } = await resolveQrRequest(parsed.data);
const body =
format === "png"
+1 -1
View File
@@ -15,7 +15,7 @@ export async function POST(request: NextRequest): Promise<Response> {
return jsonError(firstZodMessage(parsed.error));
}
const { content } = resolveQrRequest(parsed.data);
const { content } = await resolveQrRequest(parsed.data);
const svg = await renderSvg(content, parsed.data.design);
return Response.json({