QR-Typen je Gruppe freigeben
Bisher galt die Freigabe eines Typs fuer die ganze Installation. Bei mehreren Gruppen ist das zu grob: Die Marketingabteilung braucht Social Media, die Werkstatt nur Website und PDF. Die Freigabe bekommt eine zweite Ebene. Die Grundeinstellung bleibt die Grundlage; je Gruppe laesst sich davon abweichen. Fehlt eine Abweichung, folgt die Gruppe der Grundeinstellung. Zur offenen Frage im Issue: Die globale Abschaltung gewinnt immer. Ein global abgeschalteter Typ laesst sich in keiner Gruppe einschalten — der Schalter steht dort sichtbar still und nennt den Grund. Andersherum waere "hier abgeschaltet" keine verlaessliche Aussage mehr. Eine eigene Tabelle statt eines zusammengesetzten Schluessels in QrTypeSetting: Der globale Fall braeuchte dort eine leere Gruppe, und PostgreSQL laesst in einem eindeutigen Index beliebig viele NULL nebeneinander zu — die Eindeutigkeit der globalen Zeile waere also gerade nicht gesichert. Die Startseite zeigt die Vereinigung ueber die eigenen Gruppen; beim Anlegen entscheidet die gewaehlte Gruppe, und die Route prueft das noch einmal. Refs #11
This commit is contained in:
+141
-10
@@ -1,5 +1,7 @@
|
||||
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 {
|
||||
@@ -19,6 +21,16 @@ import {
|
||||
* - `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
|
||||
@@ -29,8 +41,15 @@ export type QrTypeAvailability = {
|
||||
qrType: QrTypeModule;
|
||||
/** Der Typ ist in dieser Version ausprogrammiert. */
|
||||
implemented: boolean;
|
||||
/** Die Verwaltung hat den Typ freigegeben. */
|
||||
/** 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“. */
|
||||
@@ -56,16 +75,52 @@ async function loadDisabledTypes(): Promise<ReadonlySet<string>> {
|
||||
}
|
||||
}
|
||||
|
||||
/** Verfügbarkeit aller Typen in Anzeigereihenfolge. */
|
||||
export async function getQrTypeAvailability(): Promise<QrTypeAvailability[]> {
|
||||
/** 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 enabled = !disabled.has(qrType.id);
|
||||
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,
|
||||
};
|
||||
@@ -73,15 +128,61 @@ export async function getQrTypeAvailability(): Promise<QrTypeAvailability[]> {
|
||||
}
|
||||
|
||||
/** 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 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()).find((entry) => entry.qrType.id === id);
|
||||
return (await getQrTypeAvailability(groupId)).find(
|
||||
(entry) => entry.qrType.id === id,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,8 +191,9 @@ export async function getQrTypeState(
|
||||
*/
|
||||
export async function requireAvailableQrType(
|
||||
id: string,
|
||||
groupId?: string,
|
||||
): Promise<QrTypeModule> {
|
||||
const state = await getQrTypeState(id);
|
||||
const state = await getQrTypeState(id, groupId);
|
||||
|
||||
if (!state) {
|
||||
throw new QrRequestError("Unbekannter QR-Typ.", 404);
|
||||
@@ -104,7 +206,9 @@ export async function requireAvailableQrType(
|
||||
}
|
||||
if (!state.enabled) {
|
||||
throw new QrRequestError(
|
||||
`Der QR-Typ „${state.qrType.title}“ ist in der Verwaltung abgeschaltet.`,
|
||||
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,
|
||||
);
|
||||
}
|
||||
@@ -128,7 +232,7 @@ export function requireImplementedQrType(id: string): QrTypeModule {
|
||||
return qrType;
|
||||
}
|
||||
|
||||
/** Freigabe eines Typs setzen. */
|
||||
/** Globale Freigabe eines Typs setzen. */
|
||||
export async function setQrTypeEnabled(
|
||||
id: QrTypeId,
|
||||
enabled: boolean,
|
||||
@@ -139,3 +243,30 @@ export async function setQrTypeEnabled(
|
||||
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 },
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user