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
+141
View File
@@ -0,0 +1,141 @@
import "server-only";
import { prisma } from "@/lib/db";
import { getQrType, qrTypeList } from "@/lib/qr/registry";
import {
isQrTypeId,
QrRequestError,
type QrTypeId,
type QrTypeModule,
} from "@/lib/qr/type-module";
/**
* Freigabe der QR-Typen durch die Verwaltung.
*
* Zwei voneinander unabhängige Eigenschaften bestimmen, was ein Typ kann:
*
* - `implemented` — steht in der Registry und sagt, ob der Typ überhaupt
* ausprogrammiert ist. In V1 gilt das nur für „Website“.
* - `enabled` — kommt aus der Datenbank und sagt, ob die Verwaltung den Typ
* freigegeben hat. Fehlt der Eintrag, ist der Typ freigegeben.
*
* Ein abgeschalteter Typ verschwindet aus der Oberfläche und lässt sich nicht
* mehr neu anlegen. **Bereits bestehende Codes dieses Typs funktionieren
* weiter** — sie sind gedruckt und verteilt, und ein Schalter in der
* Verwaltung darf sie nicht ins Leere laufen lassen.
*/
export type QrTypeAvailability = {
qrType: QrTypeModule;
/** Der Typ ist in dieser Version ausprogrammiert. */
implemented: boolean;
/** Die Verwaltung hat den Typ freigegeben. */
enabled: boolean;
/** Freigegeben und ausprogrammiert — nur dann ist er benutzbar. */
usable: boolean;
/** Freigegeben, aber noch nicht ausprogrammiert — „Bald verfügbar“. */
announced: boolean;
};
/** Abschaltungen aus der Datenbank; alles nicht Genannte ist freigegeben. */
async function loadDisabledTypes(): Promise<ReadonlySet<string>> {
try {
const rows = await prisma.qrTypeSetting.findMany({
where: { enabled: false },
select: { type: true },
});
return new Set(rows.map((row) => row.type));
} catch (error) {
// Ohne Datenbank ist die Anwendung für statische Codes weiterhin
// brauchbar. Lieber alles anzeigen als eine leere Startseite.
console.error(
"Freigaben der QR-Typen konnten nicht geladen werden — es gilt die Voreinstellung (alle freigegeben):",
error,
);
return new Set();
}
}
/** Verfügbarkeit aller Typen in Anzeigereihenfolge. */
export async function getQrTypeAvailability(): Promise<QrTypeAvailability[]> {
const disabled = await loadDisabledTypes();
return qrTypeList.map((qrType) => {
const enabled = !disabled.has(qrType.id);
return {
qrType,
implemented: qrType.implemented,
enabled,
usable: enabled && qrType.implemented,
announced: enabled && !qrType.implemented,
};
});
}
/** Nur die Typen, die in der Oberfläche erscheinen sollen. */
export async function getVisibleQrTypes(): Promise<QrTypeAvailability[]> {
return (await getQrTypeAvailability()).filter((entry) => entry.enabled);
}
export async function getQrTypeState(
id: string,
): Promise<QrTypeAvailability | undefined> {
if (!isQrTypeId(id)) return undefined;
return (await getQrTypeAvailability()).find((entry) => entry.qrType.id === id);
}
/**
* Liefert den Typ für das Anlegen neuer Codes — oder wirft mit einer
* Meldung, die unverändert in die Oberfläche gehen kann.
*/
export async function requireAvailableQrType(
id: string,
): Promise<QrTypeModule> {
const state = await getQrTypeState(id);
if (!state) {
throw new QrRequestError("Unbekannter QR-Typ.", 404);
}
if (!state.implemented) {
throw new QrRequestError(
`Der QR-Typ „${state.qrType.title}“ ist in dieser Version noch nicht verfügbar.`,
404,
);
}
if (!state.enabled) {
throw new QrRequestError(
`Der QR-Typ „${state.qrType.title}“ ist in der Verwaltung abgeschaltet.`,
403,
);
}
return state.qrType;
}
/**
* Typ eines bestehenden Codes. Hier zählt nur, ob der Typ ausprogrammiert
* ist — eine Abschaltung in der Verwaltung darf bestehende Codes nicht
* unbearbeitbar machen.
*/
export function requireImplementedQrType(id: string): QrTypeModule {
const qrType = getQrType(id);
if (!qrType?.implemented) {
throw new QrRequestError(
"Der Typ dieses Codes wird in dieser Version nicht unterstützt.",
404,
);
}
return qrType;
}
/** Freigabe eines Typs setzen. */
export async function setQrTypeEnabled(
id: QrTypeId,
enabled: boolean,
): Promise<void> {
await prisma.qrTypeSetting.upsert({
where: { type: id },
create: { type: id, enabled },
update: { enabled },
});
}