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:
@@ -589,6 +589,26 @@ Zwei Eigenschaften sind dabei zu unterscheiden:
|
|||||||
Nur umgesetzt **und** freigegeben heißt benutzbar. Freigegeben, aber nicht
|
Nur umgesetzt **und** freigegeben heißt benutzbar. Freigegeben, aber nicht
|
||||||
umgesetzt, erscheint als Kachel „Bald verfügbar“.
|
umgesetzt, erscheint als Kachel „Bald verfügbar“.
|
||||||
|
|
||||||
|
#### Freigabe je Gruppe
|
||||||
|
|
||||||
|
Die Auswahl **Freigabe für** über der Liste hat zwei Stufen:
|
||||||
|
|
||||||
|
- **Alle Gruppen (Grundeinstellung)** — gilt für die ganze Installation.
|
||||||
|
- **Nur: \<Gruppe\>** — weicht für diese eine Gruppe davon ab. Die
|
||||||
|
Marketingabteilung bekommt Social Media, die Werkstatt nur Website und PDF.
|
||||||
|
|
||||||
|
Fehlt eine Abweichung, folgt die Gruppe der Grundeinstellung. **Die globale
|
||||||
|
Abschaltung gewinnt immer:** Ein Typ, der in der Grundeinstellung aus ist,
|
||||||
|
lässt sich in keiner Gruppe einschalten — der Schalter steht dort sichtbar
|
||||||
|
still und sagt warum. 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.
|
||||||
|
|
||||||
|
Auf der Startseite sieht man die **Vereinigung** über die eigenen Gruppen. Wer
|
||||||
|
in Werkstatt und Marketing ist, sieht beides. Welche Freigabe gilt, entscheidet
|
||||||
|
sich erst beim Anlegen — dort zählt die gewählte Gruppe, und die Route prüft
|
||||||
|
das noch einmal.
|
||||||
|
|
||||||
> **Bereits gespeicherte Codes bleiben unberührt.** Schaltet man einen Typ ab,
|
> **Bereits gespeicherte Codes bleiben unberührt.** Schaltet man einen Typ ab,
|
||||||
> leiten vorhandene dynamische Codes weiter wie bisher und lassen sich weiter
|
> leiten vorhandene dynamische Codes weiter wie bisher und lassen sich weiter
|
||||||
> bearbeiten. Ein gedruckter Code wird durch das Abschalten seines Typs also
|
> bearbeiten. Ein gedruckter Code wird durch das Abschalten seines Typs also
|
||||||
@@ -964,8 +984,8 @@ deutschen Meldung.
|
|||||||
| `DELETE` | `/api/codes/{id}` | Code löschen |
|
| `DELETE` | `/api/codes/{id}` | Code löschen |
|
||||||
| `GET` | `/api/codes/{id}/qr` | Bild des Codes (`?format=png\|svg&download=1`) |
|
| `GET` | `/api/codes/{id}/qr` | Bild des Codes (`?format=png\|svg&download=1`) |
|
||||||
| `GET` | `/r/{slug}` | Weiterleitung, erhöht den Scan-Zähler |
|
| `GET` | `/r/{slug}` | Weiterleitung, erhöht den Scan-Zähler |
|
||||||
| `GET` | `/api/admin/types` | Zustand aller QR-Typen |
|
| `GET` | `/api/admin/types` | Zustand aller QR-Typen (`?group={id}` je Gruppe) |
|
||||||
| `PATCH` | `/api/admin/types` | Typ freigeben oder abschalten |
|
| `PATCH` | `/api/admin/types` | Typ freigeben oder abschalten (`groupId` optional) |
|
||||||
|
|
||||||
Beispiel:
|
Beispiel:
|
||||||
|
|
||||||
|
|||||||
+15
-1
@@ -466,7 +466,7 @@ auch hier ausgeschlossen, aus demselben Grund wie bei den übrigen Uploads.
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| **Einstellungen** | was Bearbeiter dürfen — etwa eigene Kurz-Links |
|
| **Einstellungen** | was Bearbeiter dürfen — etwa eigene Kurz-Links |
|
||||||
| **Erscheinungsbild** | Name, Logo, Favicon, Akzentfarbe |
|
| **Erscheinungsbild** | Name, Logo, Favicon, Akzentfarbe |
|
||||||
| **QR-Typen** | einzelne Typen ausblenden |
|
| **QR-Typen** | einzelne Typen ausblenden — für alle oder je Gruppe |
|
||||||
| **Benutzer** | anlegen, deaktivieren, Passwort setzen, Instanz-Admin vergeben |
|
| **Benutzer** | anlegen, deaktivieren, Passwort setzen, Instanz-Admin vergeben |
|
||||||
| **Gruppen** | anlegen, umbenennen, Mitglieder und deren Rollen pflegen |
|
| **Gruppen** | anlegen, umbenennen, Mitglieder und deren Rollen pflegen |
|
||||||
| **Domains** | anlegen, Gruppen zuteilen, abschalten |
|
| **Domains** | anlegen, Gruppen zuteilen, abschalten |
|
||||||
@@ -478,6 +478,20 @@ lassen sich bearbeiten. Nur neue lassen sich nicht mehr anlegen. Ohne Eintrag
|
|||||||
in der Datenbank gilt ein Typ als freigegeben, damit eine frische Installation
|
in der Datenbank gilt ein Typ als freigegeben, damit eine frische Installation
|
||||||
alles zeigt und Typen aus späteren Versionen automatisch erscheinen.
|
alles zeigt und Typen aus späteren Versionen automatisch erscheinen.
|
||||||
|
|
||||||
|
Die Freigabe hat **zwei Ebenen**. Die Grundeinstellung gilt für die ganze
|
||||||
|
Installation; je Gruppe lässt sich davon abweichen — die Marketingabteilung
|
||||||
|
bekommt Social Media, die Werkstatt nur Website und PDF. Fehlt eine Abweichung,
|
||||||
|
folgt die Gruppe der Grundeinstellung.
|
||||||
|
|
||||||
|
**Die globale Abschaltung gewinnt immer.** Ein Typ, der in der
|
||||||
|
Grundeinstellung aus ist, lässt sich in keiner Gruppe einschalten; der
|
||||||
|
Schalter steht dort sichtbar still und nennt den Grund. Andersherum wäre „hier
|
||||||
|
abgeschaltet“ keine verlässliche Aussage mehr.
|
||||||
|
|
||||||
|
Auf der Startseite sieht man die **Vereinigung** über die eigenen Gruppen. Wer
|
||||||
|
in beiden Gruppen ist, sieht beides; welche Freigabe zählt, entscheidet sich
|
||||||
|
beim Anlegen anhand der gewählten Gruppe.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Betrieb
|
## Betrieb
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
-- QR-Typen lassen sich je Gruppe abschalten.
|
||||||
|
--
|
||||||
|
-- Die globale Freigabe in "QrTypeSetting" bleibt die Grundlage. Diese Tabelle
|
||||||
|
-- traegt nur Abweichungen davon: Fehlt eine Zeile, folgt die Gruppe der
|
||||||
|
-- globalen Einstellung. Eine globale Abschaltung laesst sich hierueber nicht
|
||||||
|
-- aufheben — global aus gewinnt immer.
|
||||||
|
--
|
||||||
|
-- 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.
|
||||||
|
CREATE TABLE "QrTypeGroupSetting" (
|
||||||
|
"type" TEXT NOT NULL,
|
||||||
|
"groupId" TEXT NOT NULL,
|
||||||
|
"enabled" BOOLEAN NOT NULL,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "QrTypeGroupSetting_pkey" PRIMARY KEY ("type","groupId")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX "QrTypeGroupSetting_groupId_idx" ON "QrTypeGroupSetting"("groupId");
|
||||||
|
|
||||||
|
-- Mit der Gruppe verschwindet auch ihre Abweichung.
|
||||||
|
ALTER TABLE "QrTypeGroupSetting" ADD CONSTRAINT "QrTypeGroupSetting_groupId_fkey"
|
||||||
|
FOREIGN KEY ("groupId") REFERENCES "Group"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -87,6 +87,7 @@ model Group {
|
|||||||
memberships Membership[]
|
memberships Membership[]
|
||||||
codes QrCode[]
|
codes QrCode[]
|
||||||
domainGrants DomainGrant[]
|
domainGrants DomainGrant[]
|
||||||
|
typeSettings QrTypeGroupSetting[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mitgliedschaft eines Benutzers in einer Gruppe, mit Rolle. Ein Benutzer
|
/// Mitgliedschaft eines Benutzers in einer Gruppe, mit Rolle. Ein Benutzer
|
||||||
@@ -155,6 +156,31 @@ model QrTypeSetting {
|
|||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Abweichung von der globalen Freigabe fuer eine einzelne Gruppe.
|
||||||
|
///
|
||||||
|
/// Bewusst 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.
|
||||||
|
///
|
||||||
|
/// Fehlt eine Zeile, folgt die Gruppe der globalen Einstellung. Eine Zeile
|
||||||
|
/// mit `enabled = false` schaltet den Typ **nur** in dieser Gruppe ab. Eine
|
||||||
|
/// Zeile mit `enabled = true` kann eine globale Abschaltung **nicht**
|
||||||
|
/// aufheben — global aus gewinnt immer.
|
||||||
|
model QrTypeGroupSetting {
|
||||||
|
/// Kennung aus der Typ-Registry, z. B. "website".
|
||||||
|
type String
|
||||||
|
groupId String
|
||||||
|
enabled Boolean
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
group Group @relation(fields: [groupId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@id([type, groupId])
|
||||||
|
@@index([groupId])
|
||||||
|
}
|
||||||
|
|
||||||
/// Ein dynamischer QR-Code. Der QR-Code kodiert `{BASE_URL}/r/{slug}` und
|
/// Ein dynamischer QR-Code. Der QR-Code kodiert `{BASE_URL}/r/{slug}` und
|
||||||
/// leitet beim Aufruf auf das Ziel weiter, das aus `data` aufgelöst wird.
|
/// leitet beim Aufruf auf das Ziel weiter, das aus `data` aufgelöst wird.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -52,7 +52,12 @@ function Hinweis({
|
|||||||
export default async function CreatePage({ params }: Props) {
|
export default async function CreatePage({ params }: Props) {
|
||||||
const actor = await requirePageActor();
|
const actor = await requirePageActor();
|
||||||
const { type } = await params;
|
const { type } = await params;
|
||||||
const state = await getQrTypeState(type);
|
|
||||||
|
// Welche Gruppe gilt, entscheidet mit darueber, ob der Typ hier offensteht.
|
||||||
|
const zielgruppe = await resolveTargetGroup(actor, undefined).catch(
|
||||||
|
() => undefined,
|
||||||
|
);
|
||||||
|
const state = await getQrTypeState(type, zielgruppe);
|
||||||
|
|
||||||
if (!state) {
|
if (!state) {
|
||||||
notFound();
|
notFound();
|
||||||
@@ -63,7 +68,11 @@ export default async function CreatePage({ params }: Props) {
|
|||||||
<Hinweis
|
<Hinweis
|
||||||
icon="lock"
|
icon="lock"
|
||||||
titel={`„${state.qrType.title}“ ist abgeschaltet`}
|
titel={`„${state.qrType.title}“ ist abgeschaltet`}
|
||||||
text="Dieser QR-Typ wurde in der Verwaltung abgeschaltet. Ein Administrator kann ihn unter Verwaltung → QR-Typen wieder freigeben."
|
text={
|
||||||
|
state.enabledGlobally
|
||||||
|
? "Dieser QR-Typ ist für deine Gruppe abgeschaltet. Ein Administrator kann ihn unter Verwaltung → QR-Typen für die Gruppe wieder freigeben."
|
||||||
|
: "Dieser QR-Typ wurde in der Verwaltung abgeschaltet. Ein Administrator kann ihn unter Verwaltung → QR-Typen wieder freigeben."
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -82,8 +91,10 @@ export default async function CreatePage({ params }: Props) {
|
|||||||
// Einstellung. Das Feld erscheint nur dann — die Route prueft es ohnehin
|
// Einstellung. Das Feld erscheint nur dann — die Route prueft es ohnehin
|
||||||
// noch einmal.
|
// noch einmal.
|
||||||
const { editorsMaySetSlug } = await getSettings();
|
const { editorsMaySetSlug } = await getSettings();
|
||||||
const groupId = await resolveTargetGroup(actor, undefined).catch(() => undefined);
|
const maySetSlug = can(actor, "codes:slug", {
|
||||||
const maySetSlug = can(actor, "codes:slug", { groupId, editorsMaySetSlug });
|
groupId: zielgruppe,
|
||||||
|
editorsMaySetSlug,
|
||||||
|
});
|
||||||
|
|
||||||
return <QrGenerator typeId={state.qrType.id} maySetSlug={maySetSlug} />;
|
return <QrGenerator typeId={state.qrType.id} maySetSlug={maySetSlug} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,16 @@ import Link from "next/link";
|
|||||||
import { Icon } from "@/components/icons";
|
import { Icon } from "@/components/icons";
|
||||||
import { Badge, Notice } from "@/components/ui";
|
import { Badge, Notice } from "@/components/ui";
|
||||||
import { requirePageActor } from "@/lib/auth/guard";
|
import { requirePageActor } from "@/lib/auth/guard";
|
||||||
import { getVisibleQrTypes } from "@/lib/qr/availability";
|
import { getVisibleQrTypesForActor } from "@/lib/qr/availability";
|
||||||
|
|
||||||
// Welche Typen sichtbar sind, steht in der Datenbank und ist jederzeit
|
// Welche Typen sichtbar sind, steht in der Datenbank und ist jederzeit
|
||||||
// änderbar — die Startseite darf deshalb nicht vorgerendert werden.
|
// änderbar — die Startseite darf deshalb nicht vorgerendert werden.
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
export default async function HomePage() {
|
export default async function HomePage() {
|
||||||
await requirePageActor();
|
const actor = await requirePageActor();
|
||||||
const types = await getVisibleQrTypes();
|
// Wer in mehreren Gruppen ist, sieht die Vereinigung ihrer Freigaben.
|
||||||
|
const types = await getVisibleQrTypesForActor(actor);
|
||||||
const usableCount = types.filter((entry) => entry.usable).length;
|
const usableCount = types.filter((entry) => entry.usable).length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import Link from "next/link";
|
|||||||
import { Icon } from "@/components/icons";
|
import { Icon } from "@/components/icons";
|
||||||
import { TypeSettings, type TypeSetting } from "@/components/type-settings";
|
import { TypeSettings, type TypeSetting } from "@/components/type-settings";
|
||||||
import { requirePagePermission } from "@/lib/auth/guard";
|
import { requirePagePermission } from "@/lib/auth/guard";
|
||||||
|
import { prisma } from "@/lib/db";
|
||||||
import { getQrTypeAvailability } from "@/lib/qr/availability";
|
import { getQrTypeAvailability } from "@/lib/qr/availability";
|
||||||
|
|
||||||
export const metadata: Metadata = { title: "QR-Typen" };
|
export const metadata: Metadata = { title: "QR-Typen" };
|
||||||
@@ -15,16 +16,23 @@ export default async function TypeSettingsPage() {
|
|||||||
const availability = await getQrTypeAvailability();
|
const availability = await getQrTypeAvailability();
|
||||||
|
|
||||||
const initial: TypeSetting[] = availability.map(
|
const initial: TypeSetting[] = availability.map(
|
||||||
({ qrType, implemented, enabled }) => ({
|
({ qrType, implemented, enabled, enabledGlobally, disabledInGroup }) => ({
|
||||||
id: qrType.id,
|
id: qrType.id,
|
||||||
title: qrType.title,
|
title: qrType.title,
|
||||||
description: qrType.description,
|
description: qrType.description,
|
||||||
icon: qrType.icon,
|
icon: qrType.icon,
|
||||||
implemented,
|
implemented,
|
||||||
enabled,
|
enabled,
|
||||||
|
enabledGlobally,
|
||||||
|
disabledInGroup,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const groups = await prisma.group.findMany({
|
||||||
|
orderBy: { name: "asc" },
|
||||||
|
select: { id: true, name: true },
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-3xl px-4 py-8 sm:px-6 sm:py-10">
|
<div className="mx-auto max-w-3xl px-4 py-8 sm:px-6 sm:py-10">
|
||||||
<Link
|
<Link
|
||||||
@@ -44,6 +52,11 @@ export default async function TypeSettingsPage() {
|
|||||||
Typen sind komplett ausgeblendet und lassen sich nicht mehr neu
|
Typen sind komplett ausgeblendet und lassen sich nicht mehr neu
|
||||||
anlegen.
|
anlegen.
|
||||||
</p>
|
</p>
|
||||||
|
<p className="mt-2 text-slate-600">
|
||||||
|
Die Grundeinstellung gilt für die ganze Installation. Je Gruppe lässt
|
||||||
|
sich davon abweichen — was hier abgeschaltet ist, bleibt aber überall
|
||||||
|
abgeschaltet.
|
||||||
|
</p>
|
||||||
<p className="mt-2 text-sm text-slate-500">
|
<p className="mt-2 text-sm text-slate-500">
|
||||||
Bereits gespeicherte dynamische Codes bleiben davon unberührt: sie
|
Bereits gespeicherte dynamische Codes bleiben davon unberührt: sie
|
||||||
leiten weiter und lassen sich weiter bearbeiten. Ein gedruckter Code
|
leiten weiter und lassen sich weiter bearbeiten. Ein gedruckter Code
|
||||||
@@ -51,7 +64,7 @@ export default async function TypeSettingsPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TypeSettings initial={initial} />
|
<TypeSettings initial={initial} groups={groups} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ import { z } from "zod";
|
|||||||
import { getActor } from "@/lib/auth/actor";
|
import { getActor } from "@/lib/auth/actor";
|
||||||
import { requirePermission } from "@/lib/auth/policy";
|
import { requirePermission } from "@/lib/auth/policy";
|
||||||
import { errorResponse, firstZodMessage, jsonError } from "@/lib/http";
|
import { errorResponse, firstZodMessage, jsonError } from "@/lib/http";
|
||||||
|
import { prisma } from "@/lib/db";
|
||||||
import {
|
import {
|
||||||
getQrTypeAvailability,
|
getQrTypeAvailability,
|
||||||
setQrTypeEnabled,
|
setQrTypeEnabled,
|
||||||
|
setQrTypeEnabledForGroup,
|
||||||
} from "@/lib/qr/availability";
|
} from "@/lib/qr/availability";
|
||||||
import { QR_TYPE_IDS } from "@/lib/qr/type-module";
|
import { QR_TYPE_IDS } from "@/lib/qr/type-module";
|
||||||
|
|
||||||
@@ -15,23 +17,45 @@ export const dynamic = "force-dynamic";
|
|||||||
const updateSchema = z.object({
|
const updateSchema = z.object({
|
||||||
type: z.enum(QR_TYPE_IDS, { error: "Unbekannter QR-Typ." }),
|
type: z.enum(QR_TYPE_IDS, { error: "Unbekannter QR-Typ." }),
|
||||||
enabled: z.boolean({ error: "Der Schalter muss wahr oder falsch sein." }),
|
enabled: z.boolean({ error: "Der Schalter muss wahr oder falsch sein." }),
|
||||||
|
/**
|
||||||
|
* Für welche Gruppe der Schalter gilt. Ohne Angabe die Grundeinstellung
|
||||||
|
* für die ganze Installation.
|
||||||
|
*/
|
||||||
|
groupId: z.string().trim().min(1).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Zustand aller QR-Typen für die Verwaltung. */
|
/**
|
||||||
export async function GET(): Promise<Response> {
|
* Zustand aller QR-Typen für die Verwaltung.
|
||||||
|
*
|
||||||
|
* Mit `?group=<id>` die Sicht einer einzelnen Gruppe, sonst die
|
||||||
|
* Grundeinstellung.
|
||||||
|
*/
|
||||||
|
export async function GET(request: NextRequest): Promise<Response> {
|
||||||
try {
|
try {
|
||||||
requirePermission(await getActor(), "instance:manage");
|
requirePermission(await getActor(), "instance:manage");
|
||||||
const types = await getQrTypeAvailability();
|
|
||||||
|
const groupId = request.nextUrl.searchParams.get("group") ?? undefined;
|
||||||
|
if (groupId !== undefined) {
|
||||||
|
const gruppe = await prisma.group.findUnique({ where: { id: groupId } });
|
||||||
|
if (!gruppe) return jsonError("Diese Gruppe gibt es nicht.", 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
const types = await getQrTypeAvailability(groupId);
|
||||||
return Response.json({
|
return Response.json({
|
||||||
types: types.map(({ qrType, implemented, enabled, usable }) => ({
|
groupId: groupId ?? null,
|
||||||
id: qrType.id,
|
types: types.map(
|
||||||
title: qrType.title,
|
({ qrType, implemented, enabled, enabledGlobally, disabledInGroup, usable }) => ({
|
||||||
description: qrType.description,
|
id: qrType.id,
|
||||||
icon: qrType.icon,
|
title: qrType.title,
|
||||||
implemented,
|
description: qrType.description,
|
||||||
enabled,
|
icon: qrType.icon,
|
||||||
usable,
|
implemented,
|
||||||
})),
|
enabled,
|
||||||
|
enabledGlobally,
|
||||||
|
disabledInGroup,
|
||||||
|
usable,
|
||||||
|
}),
|
||||||
|
),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorResponse(error);
|
return errorResponse(error);
|
||||||
@@ -48,8 +72,17 @@ export async function PATCH(request: NextRequest): Promise<Response> {
|
|||||||
return jsonError(firstZodMessage(parsed.error));
|
return jsonError(firstZodMessage(parsed.error));
|
||||||
}
|
}
|
||||||
|
|
||||||
await setQrTypeEnabled(parsed.data.type, parsed.data.enabled);
|
const { type, enabled, groupId } = parsed.data;
|
||||||
return Response.json({ type: parsed.data.type, enabled: parsed.data.enabled });
|
|
||||||
|
if (groupId === undefined) {
|
||||||
|
await setQrTypeEnabled(type, enabled);
|
||||||
|
} else {
|
||||||
|
const gruppe = await prisma.group.findUnique({ where: { id: groupId } });
|
||||||
|
if (!gruppe) return jsonError("Diese Gruppe gibt es nicht.", 404);
|
||||||
|
await setQrTypeEnabledForGroup(type, groupId, enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json({ type, enabled, groupId: groupId ?? null });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorResponse(error);
|
return errorResponse(error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,7 +100,8 @@ export async function POST(request: NextRequest): Promise<Response> {
|
|||||||
// ist — und der Slug ist nur je Domain eindeutig.
|
// ist — und der Slug ist nur je Domain eindeutig.
|
||||||
const domainId = await resolveTargetDomain(actor, groupId, parsed.data.domainId);
|
const domainId = await resolveTargetDomain(actor, groupId, parsed.data.domainId);
|
||||||
|
|
||||||
const qrType = await requireAvailableQrType(type);
|
// Die Freigabe haengt an der Zielgruppe — global abgeschaltet gewinnt.
|
||||||
|
const qrType = await requireAvailableQrType(type, groupId);
|
||||||
|
|
||||||
const validated = qrType.validate(parsed.data.data);
|
const validated = qrType.validate(parsed.data.data);
|
||||||
if (!validated.ok) {
|
if (!validated.ok) {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
|
|
||||||
import { Icon, type IconName } from "@/components/icons";
|
import { Icon, type IconName } from "@/components/icons";
|
||||||
import { Badge, Card, Notice, cx } from "@/components/ui";
|
import { Badge, Card, Notice, Select, cx } from "@/components/ui";
|
||||||
import { ApiError, apiJson } from "@/lib/api-client";
|
import { ApiError, apiJson } from "@/lib/api-client";
|
||||||
import type { QrTypeId } from "@/lib/qr/type-module";
|
import type { QrTypeId } from "@/lib/qr/type-module";
|
||||||
|
|
||||||
@@ -13,9 +13,19 @@ export type TypeSetting = {
|
|||||||
description: string;
|
description: string;
|
||||||
icon: IconName;
|
icon: IconName;
|
||||||
implemented: boolean;
|
implemented: boolean;
|
||||||
|
/** Freigegeben in der gerade gezeigten Sicht (global oder Gruppe). */
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
/** Die globale Freigabe für sich genommen. */
|
||||||
|
enabledGlobally: boolean;
|
||||||
|
/** In der gezeigten Gruppe ausdrücklich abgewählt. */
|
||||||
|
disabledInGroup: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type GroupOption = { id: string; name: string };
|
||||||
|
|
||||||
|
/** Die Grundeinstellung, wenn keine Gruppe gewählt ist. */
|
||||||
|
const GLOBAL = "global";
|
||||||
|
|
||||||
function Toggle({
|
function Toggle({
|
||||||
checked,
|
checked,
|
||||||
disabled,
|
disabled,
|
||||||
@@ -50,29 +60,71 @@ function Toggle({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TypeSettings({ initial }: { initial: TypeSetting[] }) {
|
export function TypeSettings({
|
||||||
|
initial,
|
||||||
|
groups = [],
|
||||||
|
}: {
|
||||||
|
initial: TypeSetting[];
|
||||||
|
groups?: GroupOption[];
|
||||||
|
}) {
|
||||||
const [types, setTypes] = useState(initial);
|
const [types, setTypes] = useState(initial);
|
||||||
|
const [scope, setScope] = useState<string>(GLOBAL);
|
||||||
const [pending, setPending] = useState<QrTypeId | null>(null);
|
const [pending, setPending] = useState<QrTypeId | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const inGruppe = scope !== GLOBAL;
|
||||||
|
const gruppenname = groups.find((g) => g.id === scope)?.name ?? "";
|
||||||
|
|
||||||
const visibleCount = types.filter((entry) => entry.enabled).length;
|
const visibleCount = types.filter((entry) => entry.enabled).length;
|
||||||
const usableCount = types.filter(
|
const usableCount = types.filter(
|
||||||
(entry) => entry.enabled && entry.implemented,
|
(entry) => entry.enabled && entry.implemented,
|
||||||
).length;
|
).length;
|
||||||
|
|
||||||
|
/** Sicht wechseln — die Zustände unterscheiden sich je Gruppe. */
|
||||||
|
const wechsle = useCallback(async (next: string) => {
|
||||||
|
setScope(next);
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const result = await apiJson<{ types: TypeSetting[] }>(
|
||||||
|
next === GLOBAL ? "/api/admin/types" : `/api/admin/types?group=${next}`,
|
||||||
|
);
|
||||||
|
setTypes(result.types);
|
||||||
|
} catch (caught) {
|
||||||
|
setError(
|
||||||
|
caught instanceof ApiError
|
||||||
|
? caught.message
|
||||||
|
: "Die Freigaben konnten nicht geladen werden.",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const toggle = async (id: QrTypeId, enabled: boolean) => {
|
const toggle = async (id: QrTypeId, enabled: boolean) => {
|
||||||
const before = types;
|
const before = types;
|
||||||
// Sofort umschalten und bei einem Fehler zurückdrehen — der Schalter
|
// Sofort umschalten und bei einem Fehler zurückdrehen — der Schalter
|
||||||
// soll sich nicht zäh anfühlen.
|
// soll sich nicht zäh anfühlen.
|
||||||
setTypes((current) =>
|
setTypes((current) =>
|
||||||
current.map((entry) => (entry.id === id ? { ...entry, enabled } : entry)),
|
current.map((entry) =>
|
||||||
|
entry.id === id
|
||||||
|
? {
|
||||||
|
...entry,
|
||||||
|
enabled,
|
||||||
|
...(inGruppe
|
||||||
|
? { disabledInGroup: !enabled }
|
||||||
|
: { enabledGlobally: enabled }),
|
||||||
|
}
|
||||||
|
: entry,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
setPending(id);
|
setPending(id);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
await apiJson("/api/admin/types", {
|
await apiJson("/api/admin/types", {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
json: { type: id, enabled },
|
json: { type: id, enabled, ...(inGruppe ? { groupId: scope } : {}) },
|
||||||
});
|
});
|
||||||
} catch (caught) {
|
} catch (caught) {
|
||||||
setTypes(before);
|
setTypes(before);
|
||||||
@@ -90,6 +142,40 @@ export function TypeSettings({ initial }: { initial: TypeSetting[] }) {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{error ? <Notice tone="error">{error}</Notice> : null}
|
{error ? <Notice tone="error">{error}</Notice> : null}
|
||||||
|
|
||||||
|
{groups.length > 0 ? (
|
||||||
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
|
<label
|
||||||
|
htmlFor="typen-gruppe"
|
||||||
|
className="text-sm font-medium text-slate-700"
|
||||||
|
>
|
||||||
|
Freigabe für
|
||||||
|
</label>
|
||||||
|
<Select
|
||||||
|
id="typen-gruppe"
|
||||||
|
className="!w-auto"
|
||||||
|
value={scope}
|
||||||
|
disabled={loading}
|
||||||
|
onChange={(event) => void wechsle(event.target.value)}
|
||||||
|
>
|
||||||
|
<option value={GLOBAL}>Alle Gruppen (Grundeinstellung)</option>
|
||||||
|
{groups.map((group) => (
|
||||||
|
<option key={group.id} value={group.id}>
|
||||||
|
Nur: {group.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{inGruppe ? (
|
||||||
|
<Notice tone="info">
|
||||||
|
Du siehst die Freigabe für <strong>{gruppenname}</strong>. Was hier
|
||||||
|
abgeschaltet ist, gilt nur für diese Gruppe. Ein Typ, der in der
|
||||||
|
Grundeinstellung aus ist, lässt sich hier nicht einschalten — die
|
||||||
|
globale Abschaltung gewinnt immer.
|
||||||
|
</Notice>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{visibleCount === 0 ? (
|
{visibleCount === 0 ? (
|
||||||
<Notice tone="warning">
|
<Notice tone="warning">
|
||||||
Zurzeit ist kein Typ freigegeben — die Startseite bleibt leer und es
|
Zurzeit ist kein Typ freigegeben — die Startseite bleibt leer und es
|
||||||
@@ -138,17 +224,28 @@ export function TypeSettings({ initial }: { initial: TypeSetting[] }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-0.5 text-sm text-slate-500">
|
<p className="mt-0.5 text-sm text-slate-500">
|
||||||
{entry.enabled
|
{inGruppe && !entry.enabledGlobally
|
||||||
? entry.implemented
|
? "In der Grundeinstellung abgeschaltet — für diese Gruppe nicht einschaltbar."
|
||||||
? "Erscheint auf der Startseite und kann verwendet werden."
|
: entry.enabled
|
||||||
: "Erscheint auf der Startseite als „Bald verfügbar“."
|
? entry.implemented
|
||||||
: "Ausgeblendet — erscheint nicht auf der Startseite."}
|
? inGruppe
|
||||||
|
? `Steht ${gruppenname} zur Verfügung.`
|
||||||
|
: "Erscheint auf der Startseite und kann verwendet werden."
|
||||||
|
: "Erscheint auf der Startseite als „Bald verfügbar“."
|
||||||
|
: inGruppe
|
||||||
|
? `Für ${gruppenname} abgeschaltet — andere Gruppen bleiben unberührt.`
|
||||||
|
: "Ausgeblendet — erscheint nicht auf der Startseite."}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Toggle
|
<Toggle
|
||||||
checked={entry.enabled}
|
checked={entry.enabled}
|
||||||
disabled={pending === entry.id}
|
disabled={
|
||||||
|
pending === entry.id ||
|
||||||
|
loading ||
|
||||||
|
// Global aus gewinnt immer — der Schalter steht dann still.
|
||||||
|
(inGruppe && !entry.enabledGlobally)
|
||||||
|
}
|
||||||
label={`${entry.title} ${entry.enabled ? "ausblenden" : "freigeben"}`}
|
label={`${entry.title} ${entry.enabled ? "ausblenden" : "freigeben"}`}
|
||||||
onChange={(next) => void toggle(entry.id, next)}
|
onChange={(next) => void toggle(entry.id, next)}
|
||||||
/>
|
/>
|
||||||
@@ -158,8 +255,9 @@ export function TypeSettings({ initial }: { initial: TypeSetting[] }) {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<p className="text-sm text-slate-500">
|
<p className="text-sm text-slate-500">
|
||||||
{usableCount} von {types.length} Typen nutzbar, {visibleCount} auf der
|
{inGruppe
|
||||||
Startseite sichtbar.
|
? `${usableCount} von ${types.length} Typen stehen ${gruppenname} zur Verfügung.`
|
||||||
|
: `${usableCount} von ${types.length} Typen nutzbar, ${visibleCount} auf der Startseite sichtbar.`}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
+141
-10
@@ -1,5 +1,7 @@
|
|||||||
import "server-only";
|
import "server-only";
|
||||||
|
|
||||||
|
import type { Actor } from "@/lib/auth/actor";
|
||||||
|
import { visibleGroupIds } from "@/lib/auth/policy";
|
||||||
import { prisma } from "@/lib/db";
|
import { prisma } from "@/lib/db";
|
||||||
import { getQrType, qrTypeList } from "@/lib/qr/registry";
|
import { getQrType, qrTypeList } from "@/lib/qr/registry";
|
||||||
import {
|
import {
|
||||||
@@ -19,6 +21,16 @@ import {
|
|||||||
* - `enabled` — kommt aus der Datenbank und sagt, ob die Verwaltung den Typ
|
* - `enabled` — kommt aus der Datenbank und sagt, ob die Verwaltung den Typ
|
||||||
* freigegeben hat. Fehlt der Eintrag, ist der Typ freigegeben.
|
* 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
|
* Ein abgeschalteter Typ verschwindet aus der Oberfläche und lässt sich nicht
|
||||||
* mehr neu anlegen. **Bereits bestehende Codes dieses Typs funktionieren
|
* mehr neu anlegen. **Bereits bestehende Codes dieses Typs funktionieren
|
||||||
* weiter** — sie sind gedruckt und verteilt, und ein Schalter in der
|
* weiter** — sie sind gedruckt und verteilt, und ein Schalter in der
|
||||||
@@ -29,8 +41,15 @@ export type QrTypeAvailability = {
|
|||||||
qrType: QrTypeModule;
|
qrType: QrTypeModule;
|
||||||
/** Der Typ ist in dieser Version ausprogrammiert. */
|
/** Der Typ ist in dieser Version ausprogrammiert. */
|
||||||
implemented: boolean;
|
implemented: boolean;
|
||||||
/** Die Verwaltung hat den Typ freigegeben. */
|
/** Die Verwaltung hat den Typ freigegeben — global und in der Gruppe. */
|
||||||
enabled: boolean;
|
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. */
|
/** Freigegeben und ausprogrammiert — nur dann ist er benutzbar. */
|
||||||
usable: boolean;
|
usable: boolean;
|
||||||
/** Freigegeben, aber noch nicht ausprogrammiert — „Bald verfügbar“. */
|
/** Freigegeben, aber noch nicht ausprogrammiert — „Bald verfügbar“. */
|
||||||
@@ -56,16 +75,52 @@ async function loadDisabledTypes(): Promise<ReadonlySet<string>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Verfügbarkeit aller Typen in Anzeigereihenfolge. */
|
/** Abweichungen einer Gruppe; leer, wenn sie der globalen Einstellung folgt. */
|
||||||
export async function getQrTypeAvailability(): Promise<QrTypeAvailability[]> {
|
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 disabled = await loadDisabledTypes();
|
||||||
|
const inGruppeAbgewaehlt =
|
||||||
|
groupId === undefined
|
||||||
|
? new Set<string>()
|
||||||
|
: await loadGroupDisabledTypes(groupId);
|
||||||
|
|
||||||
return qrTypeList.map((qrType) => {
|
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 {
|
return {
|
||||||
qrType,
|
qrType,
|
||||||
implemented: qrType.implemented,
|
implemented: qrType.implemented,
|
||||||
enabled,
|
enabled,
|
||||||
|
enabledGlobally: global,
|
||||||
|
disabledInGroup: inGruppe,
|
||||||
usable: enabled && qrType.implemented,
|
usable: enabled && qrType.implemented,
|
||||||
announced: 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. */
|
/** Nur die Typen, die in der Oberfläche erscheinen sollen. */
|
||||||
export async function getVisibleQrTypes(): Promise<QrTypeAvailability[]> {
|
export async function getVisibleQrTypes(
|
||||||
return (await getQrTypeAvailability()).filter((entry) => entry.enabled);
|
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(
|
export async function getQrTypeState(
|
||||||
id: string,
|
id: string,
|
||||||
|
groupId?: string,
|
||||||
): Promise<QrTypeAvailability | undefined> {
|
): Promise<QrTypeAvailability | undefined> {
|
||||||
if (!isQrTypeId(id)) return 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(
|
export async function requireAvailableQrType(
|
||||||
id: string,
|
id: string,
|
||||||
|
groupId?: string,
|
||||||
): Promise<QrTypeModule> {
|
): Promise<QrTypeModule> {
|
||||||
const state = await getQrTypeState(id);
|
const state = await getQrTypeState(id, groupId);
|
||||||
|
|
||||||
if (!state) {
|
if (!state) {
|
||||||
throw new QrRequestError("Unbekannter QR-Typ.", 404);
|
throw new QrRequestError("Unbekannter QR-Typ.", 404);
|
||||||
@@ -104,7 +206,9 @@ export async function requireAvailableQrType(
|
|||||||
}
|
}
|
||||||
if (!state.enabled) {
|
if (!state.enabled) {
|
||||||
throw new QrRequestError(
|
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,
|
403,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -128,7 +232,7 @@ export function requireImplementedQrType(id: string): QrTypeModule {
|
|||||||
return qrType;
|
return qrType;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Freigabe eines Typs setzen. */
|
/** Globale Freigabe eines Typs setzen. */
|
||||||
export async function setQrTypeEnabled(
|
export async function setQrTypeEnabled(
|
||||||
id: QrTypeId,
|
id: QrTypeId,
|
||||||
enabled: boolean,
|
enabled: boolean,
|
||||||
@@ -139,3 +243,30 @@ export async function setQrTypeEnabled(
|
|||||||
update: { 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 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user