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> { 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> { 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 { const disabled = await loadDisabledTypes(); const inGruppeAbgewaehlt = groupId === undefined ? new Set() : 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 { 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 { 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(); 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 { 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 { 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 { 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 { 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 }, }); }