Files
qrcode-generator/src/lib/qr/availability.ts
T
Florian Wilde cdda7df17a Version 1.7.0: Doku durchgesehen und Roadmap angeglichen
Sechs Erweiterungen sind zusammengekommen — Logo im Code, Freigabe je
Gruppe, Domain-Check, Dublettenpruefung, Filter nach Art und die
Kurzhilfe zu den Domains. Dafuer eine neue Minor-Version.

Zwei Stellen waren schlicht falsch und wiegen schwerer als jede Luecke:
Anleitung und Funktionsuebersicht behaupteten beide noch, statische Codes
wuerden "nicht gespeichert" — ueberholt seit 1.5.0. Ebenso die Aussage,
nur "Website" sei ausprogrammiert; das gilt seit 1.1.0 fuer alle fuenfzehn.

Danach die Luecken: Jedes der sechs Features steht jetzt in allen drei
Dokumenten, die es betrifft. Die Bedienungsanleitung hatte keines davon.

Die Roadmap verliert die zwei gebauten Punkte und bekommt einen neuen aus
der Umsetzung: Die Gestaltung eines gespeicherten Codes laesst sich
ueberhaupt nicht aendern, ein Logo also nur ueber die Schnittstelle
nachruesten.

Die Kurzhilfe verwies unter "Nachpruefen" noch auf curl, obwohl seit #7
der Pruefen-Knopf danebensteht.
2026-08-21 11:27:15 +02:00

274 lines
8.6 KiB
TypeScript

import "server-only";
import type { Actor } from "@/lib/auth/actor";
import { visibleGroupIds } from "@/lib/auth/policy";
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. Seit 1.1.0 gilt das für alle fünfzehn; die
* Unterscheidung bleibt für Typen, die später dazukommen.
* - `enabled` — kommt aus der Datenbank und sagt, ob die Verwaltung den Typ
* freigegeben hat. Fehlt der Eintrag, ist der Typ freigegeben.
*
* Die Freigabe hat zwei Ebenen. **Global** gilt für die ganze Installation
* und ist die Grundlage; **je Gruppe** lässt sich davon abweichen, damit die
* Marketingabteilung Social Media bekommt und die Werkstatt nur Website und
* PDF. Fehlt eine Abweichung, folgt die Gruppe der globalen Einstellung.
*
* **Die globale Abschaltung gewinnt immer.** Ein global abgeschalteter Typ
* lässt sich in keiner Gruppe wieder einschalten. Andersherum wäre „hier
* abgeschaltet“ keine verlässliche Aussage mehr, und wer den Schalter umlegt,
* müsste alle Gruppen durchsehen, um zu wissen, was er getan hat.
*
* 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 — global und in der Gruppe. */
enabled: boolean;
/** Die globale Freigabe für sich genommen, ohne die Gruppe. */
enabledGlobally: boolean;
/**
* Nur bei einer Abfrage für eine bestimmte Gruppe gesetzt: Ist der Typ dort
* ausdrücklich abgewählt? Ohne Abweichung `false`.
*/
disabledInGroup: 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();
}
}
/** Abweichungen einer Gruppe; leer, wenn sie der globalen Einstellung folgt. */
async function loadGroupDisabledTypes(
groupId: string,
): Promise<ReadonlySet<string>> {
try {
const rows = await prisma.qrTypeGroupSetting.findMany({
where: { groupId, enabled: false },
select: { type: true },
});
return new Set(rows.map((row) => row.type));
} catch (error) {
// Wie oben: im Zweifel lieber alles zeigen als eine leere Startseite.
console.error(
"Freigaben der QR-Typen je Gruppe konnten nicht geladen werden — es gilt die globale Einstellung:",
error,
);
return new Set();
}
}
/**
* Verfügbarkeit aller Typen in Anzeigereihenfolge.
*
* Ohne `groupId` gilt allein die globale Freigabe — das ist die Sicht der
* Verwaltung auf die Grundeinstellung.
*/
export async function getQrTypeAvailability(
groupId?: string,
): Promise<QrTypeAvailability[]> {
const disabled = await loadDisabledTypes();
const inGruppeAbgewaehlt =
groupId === undefined
? new Set<string>()
: await loadGroupDisabledTypes(groupId);
return qrTypeList.map((qrType) => {
const global = !disabled.has(qrType.id);
const inGruppe = inGruppeAbgewaehlt.has(qrType.id);
// Global aus gewinnt immer — eine Gruppe kann das nicht aufheben.
const enabled = global && !inGruppe;
return {
qrType,
implemented: qrType.implemented,
enabled,
enabledGlobally: global,
disabledInGroup: inGruppe,
usable: enabled && qrType.implemented,
announced: enabled && !qrType.implemented,
};
});
}
/** Nur die Typen, die in der Oberfläche erscheinen sollen. */
export async function getVisibleQrTypes(
groupId?: string,
): Promise<QrTypeAvailability[]> {
return (await getQrTypeAvailability(groupId)).filter((entry) => entry.enabled);
}
/**
* Was jemand auf der Startseite sehen soll: die **Vereinigung** über seine
* Gruppen.
*
* Wer in der Werkstatt und im Marketing ist, sieht beides. Welche Gruppe
* beim Anlegen gilt, entscheidet sich erst dort — und wird dort auch geprüft.
*/
export async function getVisibleQrTypesForActor(
actor: Actor,
): Promise<QrTypeAvailability[]> {
const eigene = visibleGroupIds(actor);
// `null` heißt: alle Gruppen. Ohne Gruppen (oder ohne Anmeldung) bleibt es
// bei der globalen Einstellung.
const gruppen =
eigene === null
? (await prisma.group.findMany({ select: { id: true } })).map((g) => g.id)
: eigene;
if (gruppen.length === 0) return getVisibleQrTypes();
const je = await Promise.all(gruppen.map((id) => getVisibleQrTypes(id)));
const gesehen = new Set<string>();
const vereinigt: QrTypeAvailability[] = [];
// Über qrTypeList laufen statt über die Ergebnisse: so bleibt die
// Anzeigereihenfolge der Registry erhalten.
for (const qrType of qrTypeList) {
for (const liste of je) {
if (gesehen.has(qrType.id)) break;
const treffer = liste.find((entry) => entry.qrType.id === qrType.id);
if (treffer) {
gesehen.add(qrType.id);
vereinigt.push(treffer);
}
}
}
return vereinigt;
}
export async function getQrTypeState(
id: string,
groupId?: string,
): Promise<QrTypeAvailability | undefined> {
if (!isQrTypeId(id)) return undefined;
return (await getQrTypeAvailability(groupId)).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,
groupId?: string,
): Promise<QrTypeModule> {
const state = await getQrTypeState(id, groupId);
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(
state.enabledGlobally
? `Der QR-Typ „${state.qrType.title}“ ist für diese Gruppe abgeschaltet.`
: `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;
}
/** Globale 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 },
});
}
/**
* Freigabe eines Typs für eine Gruppe setzen.
*
* `true` löscht die Abweichung, statt sie auf „an“ zu setzen: Die Gruppe
* folgt dann wieder der globalen Einstellung. Eine Zeile „an“ zu hinterlassen
* würde vortäuschen, sie könnte eine globale Abschaltung überstimmen — das
* kann sie nicht.
*/
export async function setQrTypeEnabledForGroup(
id: QrTypeId,
groupId: string,
enabled: boolean,
): Promise<void> {
if (enabled) {
await prisma.qrTypeGroupSetting.deleteMany({
where: { type: id, groupId },
});
return;
}
await prisma.qrTypeGroupSetting.upsert({
where: { type_groupId: { type: id, groupId } },
create: { type: id, groupId, enabled: false },
update: { enabled: false },
});
}