Aktenzeichen-Muster je Vorlage, Startskript für die Entwicklung
Richtigstellung zuerst: der vorige Commit trägt die Nachricht "Nichts davon gebaut", enthält aber bereits die Schema-Änderung an CaseTemplate und CaseCounter. Ein abgebrochener Aufruf hatte die Datei schon geschrieben, und das nachfolgende `git add -A` hat sie mitgenommen. Die Nachricht war damit falsch. Hier ist die Änderung vollständig und geprüft. Das Muster des Aktenzeichens gehört zur Vorlage: Präfix und Stellenzahl je Fallart, ein Nummernkreis je Präfix und Jahr. Die Abteilung führt gewachsene Präfixe wie AKT-PA-2026-62843 neben dem AfiS-2026-0042 aus dem Glossar. Die Migration ist von Hand geschrieben, weil der Wechsel des Primärschlüssels auf CaseCounter den bestehenden Zähler erhalten muss: eine zurückgesetzte Nummer wäre ein doppeltes Aktenzeichen. Vorher und nachher geprüft. parseCaseNumber zerlegt von hinten, damit Präfixe mit Bindestrich funktionieren. Ein Test belegt, dass AKT-PA und AfiS getrennte Nummernkreise führen. Dazu start.sh für die lokale Entwicklung, solange der Dockge-Stack nicht läuft: Datenbank hochfahren, Migrationen, Grundzustand, Worker und Web. Mit --ohne-worker für Arbeit ohne Discord und --nur-db für die Datenbank allein.
This commit is contained in:
@@ -0,0 +1,20 @@
|
|||||||
|
-- Aktenzeichen-Muster je Vorlage, siehe docs/13-entscheidungen.md.
|
||||||
|
--
|
||||||
|
-- Die Abteilung führt gewachsene Präfixe wie "AKT-PA" mit fünfstelliger
|
||||||
|
-- Nummer. Das Muster gehört deshalb zur Vorlage, nicht ins Programm.
|
||||||
|
|
||||||
|
ALTER TABLE "CaseTemplate"
|
||||||
|
ADD COLUMN "numberPrefix" TEXT NOT NULL DEFAULT 'AfiS',
|
||||||
|
ADD COLUMN "numberPadding" INTEGER NOT NULL DEFAULT 4;
|
||||||
|
|
||||||
|
-- Der Nummernkreis läuft ab jetzt je Präfix und Jahr. Bestehende Zähler
|
||||||
|
-- gehören zum bisherigen Präfix "AfiS" und werden nicht zurückgesetzt:
|
||||||
|
-- eine erneut vergebene Nummer wäre ein doppeltes Aktenzeichen.
|
||||||
|
ALTER TABLE "CaseCounter" ADD COLUMN "prefix" TEXT NOT NULL DEFAULT 'AfiS';
|
||||||
|
|
||||||
|
ALTER TABLE "CaseCounter" DROP CONSTRAINT "CaseCounter_pkey";
|
||||||
|
ALTER TABLE "CaseCounter" ADD CONSTRAINT "CaseCounter_pkey" PRIMARY KEY ("prefix", "year");
|
||||||
|
|
||||||
|
-- Der Vorgabewert hat nur die vorhandenen Zeilen umgestellt; neue Zeilen
|
||||||
|
-- nennen ihr Präfix ausdrücklich.
|
||||||
|
ALTER TABLE "CaseCounter" ALTER COLUMN "prefix" DROP DEFAULT;
|
||||||
@@ -36,7 +36,13 @@ const CLEARANCE_LEVELS = [
|
|||||||
* mit allen Feldtypen baut Phase 3, hier steht nur, was eine Akte zum Laufen
|
* mit allen Feldtypen baut Phase 3, hier steht nur, was eine Akte zum Laufen
|
||||||
* braucht.
|
* braucht.
|
||||||
*/
|
*/
|
||||||
const TEMPLATES = [
|
const TEMPLATES: {
|
||||||
|
key: string
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
numberPrefix?: string
|
||||||
|
numberPadding?: number
|
||||||
|
}[] = [
|
||||||
{
|
{
|
||||||
key: 'korruptionsverdacht',
|
key: 'korruptionsverdacht',
|
||||||
name: 'Korruptionsverdacht',
|
name: 'Korruptionsverdacht',
|
||||||
@@ -96,18 +102,32 @@ async function main(): Promise<void> {
|
|||||||
for (const template of TEMPLATES) {
|
for (const template of TEMPLATES) {
|
||||||
await db.caseTemplate.upsert({
|
await db.caseTemplate.upsert({
|
||||||
where: { key_version: { key: template.key, version: 1 } },
|
where: { key_version: { key: template.key, version: 1 } },
|
||||||
create: { ...template, version: 1, fields: TEMPLATE_FIELDS, isActive: true },
|
create: {
|
||||||
|
...template,
|
||||||
|
numberPrefix: template.numberPrefix ?? 'AfiS',
|
||||||
|
numberPadding: template.numberPadding ?? 4,
|
||||||
|
version: 1,
|
||||||
|
fields: TEMPLATE_FIELDS,
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
// Muster und Felder bleiben unangetastet: was der Leiter im
|
||||||
|
// Vorlagen-Editor eingestellt hat, überschreibt der Seed nicht.
|
||||||
update: { name: template.name, description: template.description },
|
update: { name: template.name, description: template.description },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
console.log(`[seed] ${TEMPLATES.length} Startvorlagen`)
|
console.log(`[seed] ${TEMPLATES.length} Startvorlagen`)
|
||||||
|
|
||||||
|
// Ein Nummernkreis je Präfix und Jahr. Angelegt wird er beim ersten Fall
|
||||||
|
// von selbst; hier steht er nur, damit ein frischer Bestand nachvollziehbar
|
||||||
|
// bei null beginnt.
|
||||||
const year = new Date().getFullYear()
|
const year = new Date().getFullYear()
|
||||||
|
for (const prefix of [...new Set(TEMPLATES.map((t) => t.numberPrefix ?? 'AfiS'))]) {
|
||||||
await db.caseCounter.upsert({
|
await db.caseCounter.upsert({
|
||||||
where: { year },
|
where: { prefix_year: { prefix, year } },
|
||||||
create: { year, lastNumber: 0 },
|
create: { prefix, year, lastNumber: 0 },
|
||||||
update: {},
|
update: {},
|
||||||
})
|
})
|
||||||
|
}
|
||||||
console.log(`[seed] Aktenzeichen-Zähler für ${year}`)
|
console.log(`[seed] Aktenzeichen-Zähler für ${year}`)
|
||||||
|
|
||||||
// entry.edit_own, entry.edit_any und entry.retract sind bewusst keiner Rolle
|
// entry.edit_own, entry.edit_any und entry.retract sind bewusst keiner Rolle
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Startet die Anwendung für die lokale Entwicklung: Datenbank, Worker, Web.
|
||||||
|
#
|
||||||
|
# Das hier ist ausdrücklich **nicht** der Betrieb. Im Betrieb läuft alles als
|
||||||
|
# ein Dockge-Stack, siehe docs/11-betrieb.md. Dieses Skript ist für den Fall,
|
||||||
|
# dass der Stack gerade nicht läuft und man am Code arbeiten will.
|
||||||
|
#
|
||||||
|
# ./start.sh Datenbank, Worker und Web starten
|
||||||
|
# ./start.sh --ohne-worker nur Datenbank und Web (kein Discord nötig)
|
||||||
|
# ./start.sh --nur-db nur die Datenbank hochfahren
|
||||||
|
#
|
||||||
|
# Beenden mit Strg+C, das nimmt alle Kindprozesse mit.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
DB_CONTAINER="afis-dev-db"
|
||||||
|
DB_PORT="${AFIS_DB_PORT:-5434}"
|
||||||
|
DB_IMAGE="postgres:16-alpine"
|
||||||
|
|
||||||
|
MIT_WORKER=1
|
||||||
|
NUR_DB=0
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--ohne-worker) MIT_WORKER=0 ;;
|
||||||
|
--nur-db) NUR_DB=1 ;;
|
||||||
|
-h|--help) awk 'NR>1 && /^#/ { sub(/^# ?/, ""); print; next } NR>1 { exit }' "$0"; exit 0 ;;
|
||||||
|
*) echo "Unbekannte Option: $arg" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
info() { printf '\033[36m▸\033[0m %s\n' "$*"; }
|
||||||
|
warn() { printf '\033[33m!\033[0m %s\n' "$*"; }
|
||||||
|
fehler() { printf '\033[31m✗\033[0m %s\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
|
# --- Node aus nvm holen, falls es nicht schon im PATH liegt -------------------
|
||||||
|
if ! command -v node >/dev/null 2>&1; then
|
||||||
|
if [ -s "$HOME/.nvm/nvm.sh" ]; then
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
. "$HOME/.nvm/nvm.sh" >/dev/null
|
||||||
|
nvm use --lts >/dev/null 2>&1 || nvm use node >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
command -v node >/dev/null 2>&1 || fehler "Node nicht gefunden. Node 22 installieren oder nvm laden."
|
||||||
|
|
||||||
|
if ! command -v pnpm >/dev/null 2>&1; then
|
||||||
|
corepack enable >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
command -v pnpm >/dev/null 2>&1 || fehler "pnpm nicht gefunden. 'corepack enable' ausführen."
|
||||||
|
|
||||||
|
command -v docker >/dev/null 2>&1 || fehler "Docker nicht gefunden, die Datenbank läuft im Container."
|
||||||
|
|
||||||
|
[ -f .env ] || fehler ".env fehlt. Von .env.example abschreiben und die Discord-Werte eintragen."
|
||||||
|
|
||||||
|
# --- Datenbank ---------------------------------------------------------------
|
||||||
|
if [ -n "$(docker ps -q -f "name=^${DB_CONTAINER}$")" ]; then
|
||||||
|
info "Datenbank läuft bereits"
|
||||||
|
elif [ -n "$(docker ps -aq -f "name=^${DB_CONTAINER}$")" ]; then
|
||||||
|
info "Datenbank starten"
|
||||||
|
docker start "$DB_CONTAINER" >/dev/null
|
||||||
|
else
|
||||||
|
info "Datenbank anlegen (Port ${DB_PORT})"
|
||||||
|
docker run -d --name "$DB_CONTAINER" \
|
||||||
|
-e POSTGRES_DB=afis -e POSTGRES_USER=afis -e POSTGRES_PASSWORD=afis \
|
||||||
|
-p "${DB_PORT}:5432" "$DB_IMAGE" >/dev/null \
|
||||||
|
|| fehler "Container nicht gestartet. Läuft schon etwas auf Port ${DB_PORT}?"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf ' warte auf Postgres'
|
||||||
|
for _ in $(seq 1 60); do
|
||||||
|
if docker exec "$DB_CONTAINER" pg_isready -U afis -d afis >/dev/null 2>&1; then
|
||||||
|
printf ' bereit\n'
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
printf '.'
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
docker exec "$DB_CONTAINER" pg_isready -U afis -d afis >/dev/null 2>&1 \
|
||||||
|
|| fehler "Postgres antwortet nicht."
|
||||||
|
|
||||||
|
# --- Schema und Grundzustand -------------------------------------------------
|
||||||
|
info "Prisma-Client erzeugen"
|
||||||
|
pnpm --filter @afis/db run build >/dev/null
|
||||||
|
|
||||||
|
info "Migrationen anwenden"
|
||||||
|
pnpm db:deploy >/dev/null
|
||||||
|
|
||||||
|
info "Grundzustand einspielen"
|
||||||
|
pnpm db:seed >/dev/null
|
||||||
|
|
||||||
|
if [ "$NUR_DB" = "1" ]; then
|
||||||
|
info "Datenbank läuft auf localhost:${DB_PORT}. Beenden mit: docker stop ${DB_CONTAINER}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Kindprozesse ------------------------------------------------------------
|
||||||
|
PIDS=()
|
||||||
|
aufraeumen() {
|
||||||
|
echo
|
||||||
|
info "Beenden"
|
||||||
|
for pid in "${PIDS[@]:-}"; do
|
||||||
|
kill "$pid" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
wait 2>/dev/null || true
|
||||||
|
info "Die Datenbank läuft weiter. Anhalten mit: docker stop ${DB_CONTAINER}"
|
||||||
|
}
|
||||||
|
trap aufraeumen INT TERM EXIT
|
||||||
|
|
||||||
|
if [ "$MIT_WORKER" = "1" ]; then
|
||||||
|
info "Worker starten (Discord-Rollenabgleich)"
|
||||||
|
pnpm worker:dev 2>&1 | sed 's/^/[worker] /' &
|
||||||
|
PIDS+=($!)
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Web starten"
|
||||||
|
pnpm dev 2>&1 | sed 's/^/[web] /' &
|
||||||
|
PIDS+=($!)
|
||||||
|
|
||||||
|
echo
|
||||||
|
info "Anwendung unter http://localhost:3000"
|
||||||
|
[ "$MIT_WORKER" = "1" ] || warn "Ohne Worker: Rollen werden nicht abgeglichen, /api/health meldet das Gateway als gestört"
|
||||||
|
echo
|
||||||
|
|
||||||
|
wait
|
||||||
+36
-19
@@ -1,40 +1,57 @@
|
|||||||
import type { Prisma } from '@afis/db'
|
import type { Prisma } from '@afis/db'
|
||||||
|
|
||||||
/** `AfiS-2026-0042`, siehe Glossar in docs/01-produkt.md. */
|
/**
|
||||||
export function formatCaseNumber(year: number, number: number): string {
|
* Aktenzeichen. Das Muster gehört zur Vorlage, nicht ins Programm: die
|
||||||
return `AfiS-${year}-${String(number).padStart(4, '0')}`
|
* Abteilung führt gewachsene Präfixe wie `AKT-PA-2026-62843` neben dem
|
||||||
|
* `AfiS-2026-0042` aus dem Glossar. Siehe docs/13-entscheidungen.md.
|
||||||
|
*/
|
||||||
|
export type NumberPattern = { prefix: string; padding: number }
|
||||||
|
|
||||||
|
export const DEFAULT_PATTERN: NumberPattern = { prefix: 'AfiS', padding: 4 }
|
||||||
|
|
||||||
|
export function formatCaseNumber(
|
||||||
|
year: number,
|
||||||
|
number: number,
|
||||||
|
pattern: NumberPattern = DEFAULT_PATTERN,
|
||||||
|
): string {
|
||||||
|
return `${pattern.prefix}-${year}-${String(number).padStart(pattern.padding, '0')}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const PATTERN = /^AfiS-(\d{4})-(\d{4})$/
|
/**
|
||||||
|
* Ein Präfix darf Bindestriche enthalten (`AKT-PA`). Zerlegt wird deshalb von
|
||||||
|
* hinten: die letzten beiden Abschnitte sind Jahr und laufende Nummer, alles
|
||||||
|
* davor ist das Präfix.
|
||||||
|
*/
|
||||||
|
const PATTERN = /^([A-Za-z][A-Za-z0-9-]*)-(\d{4})-(\d+)$/
|
||||||
|
|
||||||
export function parseCaseNumber(value: string): { year: number; number: number } | null {
|
export function parseCaseNumber(
|
||||||
|
value: string,
|
||||||
|
): { prefix: string; year: number; number: number } | null {
|
||||||
const match = PATTERN.exec(value.trim())
|
const match = PATTERN.exec(value.trim())
|
||||||
if (!match) return null
|
if (!match) return null
|
||||||
return { year: Number(match[1]), number: Number(match[2]) }
|
return { prefix: match[1]!, year: Number(match[2]), number: Number(match[3]) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Erkennt Aktenzeichen im Fliesstext, für die Vorschaukarten im Chat. */
|
/** Erkennt Aktenzeichen im Fliesstext, für die Vorschaukarten im Chat. */
|
||||||
export const CASE_NUMBER_GLOBAL = /AfiS-\d{4}-\d{4}/g
|
export const CASE_NUMBER_GLOBAL = /\b[A-Za-z][A-Za-z0-9-]*-\d{4}-\d{3,8}\b/g
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vergibt das nächste Aktenzeichen des Jahres.
|
* Vergibt das nächste Aktenzeichen im Nummernkreis dieses Präfixes und Jahres.
|
||||||
*
|
*
|
||||||
* Läuft mit `SELECT … FOR UPDATE` in **derselben** Transaktion wie das Anlegen
|
* `INSERT … ON CONFLICT DO UPDATE` sperrt die Zeile für die Dauer der
|
||||||
* des Falls. Zwei gleichzeitige Anlagen warten dadurch aufeinander, statt
|
* Transaktion. Zwei gleichzeitige Anlagen warten dadurch aufeinander, statt
|
||||||
* dieselbe Nummer zu ziehen oder eine Lücke zu hinterlassen.
|
* dieselbe Nummer zu ziehen. Läuft in **derselben** Transaktion wie das
|
||||||
* Siehe docs/04-datenmodell.md.
|
* Anlegen des Falls, siehe docs/04-datenmodell.md.
|
||||||
*
|
|
||||||
* Eine Sequenz pro Jahr wäre einfacher, müsste aber jedes Jahr von Hand
|
|
||||||
* angelegt werden — und der Abteilungsleiter ist ausdrücklich nicht technisch.
|
|
||||||
*/
|
*/
|
||||||
export async function nextCaseNumber(
|
export async function nextCaseNumber(
|
||||||
tx: Prisma.TransactionClient,
|
tx: Prisma.TransactionClient,
|
||||||
|
pattern: NumberPattern = DEFAULT_PATTERN,
|
||||||
year = new Date().getFullYear(),
|
year = new Date().getFullYear(),
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const rows = await tx.$queryRaw<{ lastNumber: number }[]>`
|
const rows = await tx.$queryRaw<{ lastNumber: number }[]>`
|
||||||
INSERT INTO "CaseCounter" ("year", "lastNumber")
|
INSERT INTO "CaseCounter" ("prefix", "year", "lastNumber")
|
||||||
VALUES (${year}, 1)
|
VALUES (${pattern.prefix}, ${year}, 1)
|
||||||
ON CONFLICT ("year") DO UPDATE
|
ON CONFLICT ("prefix", "year") DO UPDATE
|
||||||
SET "lastNumber" = "CaseCounter"."lastNumber" + 1
|
SET "lastNumber" = "CaseCounter"."lastNumber" + 1
|
||||||
RETURNING "lastNumber"
|
RETURNING "lastNumber"
|
||||||
`
|
`
|
||||||
@@ -44,5 +61,5 @@ export async function nextCaseNumber(
|
|||||||
throw new Error('Aktenzeichen konnte nicht vergeben werden')
|
throw new Error('Aktenzeichen konnte nicht vergeben werden')
|
||||||
}
|
}
|
||||||
|
|
||||||
return formatCaseNumber(year, lastNumber)
|
return formatCaseNumber(year, lastNumber, pattern)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,12 +56,16 @@ export async function createCase(
|
|||||||
const template = await db.caseTemplate.findFirst({
|
const template = await db.caseTemplate.findFirst({
|
||||||
where: { key: input.templateKey, isActive: true },
|
where: { key: input.templateKey, isActive: true },
|
||||||
orderBy: { version: 'desc' },
|
orderBy: { version: 'desc' },
|
||||||
select: { id: true, key: true },
|
select: { id: true, key: true, numberPrefix: true, numberPadding: true },
|
||||||
})
|
})
|
||||||
if (!template) throw new AccessError('template-missing')
|
if (!template) throw new AccessError('template-missing')
|
||||||
|
|
||||||
return db.$transaction(async (tx) => {
|
return db.$transaction(async (tx) => {
|
||||||
const caseNumber = await nextCaseNumber(tx)
|
// Das Muster kommt aus der Vorlage, jede Fallart hat ihren Nummernkreis.
|
||||||
|
const caseNumber = await nextCaseNumber(tx, {
|
||||||
|
prefix: template.numberPrefix,
|
||||||
|
padding: template.numberPadding,
|
||||||
|
})
|
||||||
|
|
||||||
const created = await tx.case.create({
|
const created = await tx.case.create({
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -89,6 +89,51 @@ suite('Schreibgrenzen der Freigabestufe', () => {
|
|||||||
expect(new Set(nummern).size).toBe(nummern.length)
|
expect(new Set(nummern).size).toBe(nummern.length)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('führt je Präfix einen eigenen Nummernkreis', async () => {
|
||||||
|
// Die Abteilung nutzt gewachsene Präfixe wie AKT-PA neben dem AfiS aus dem
|
||||||
|
// Glossar. Zwei Fallarten dürfen sich dabei nicht in die Quere kommen.
|
||||||
|
const { createCase } = await import('@/lib/cases/service')
|
||||||
|
await db.caseTemplate.create({
|
||||||
|
data: {
|
||||||
|
key: `${marke}-personal`,
|
||||||
|
version: 1,
|
||||||
|
name: 'Personalvorgang',
|
||||||
|
fields: [],
|
||||||
|
numberPrefix: 'AKT-PA',
|
||||||
|
numberPadding: 5,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const ersteA = await createCase(perms(5, VOLL), {
|
||||||
|
title: 'Personalvorgang eins',
|
||||||
|
classification: 1,
|
||||||
|
templateKey: `${marke}-personal`,
|
||||||
|
})
|
||||||
|
const zweiteA = await createCase(perms(5, VOLL), {
|
||||||
|
title: 'Personalvorgang zwei',
|
||||||
|
classification: 1,
|
||||||
|
templateKey: `${marke}-personal`,
|
||||||
|
})
|
||||||
|
|
||||||
|
const jahr = new Date().getFullYear()
|
||||||
|
expect(ersteA.caseNumber).toMatch(new RegExp(`^AKT-PA-${jahr}-\\d{5}$`))
|
||||||
|
expect(zweiteA.caseNumber).toMatch(new RegExp(`^AKT-PA-${jahr}-\\d{5}$`))
|
||||||
|
|
||||||
|
const { parseCaseNumber } = await import('@/lib/cases/caseNumber')
|
||||||
|
const eins = parseCaseNumber(ersteA.caseNumber)
|
||||||
|
const zwei = parseCaseNumber(zweiteA.caseNumber)
|
||||||
|
expect(eins?.prefix).toBe('AKT-PA')
|
||||||
|
expect(zwei!.number).toBe(eins!.number + 1)
|
||||||
|
|
||||||
|
// Der AfiS-Kreis läuft davon unberührt weiter.
|
||||||
|
const afis = await createCase(perms(5, VOLL), {
|
||||||
|
title: 'Ermittlung',
|
||||||
|
classification: 1,
|
||||||
|
templateKey: `${marke}-vorlage`,
|
||||||
|
})
|
||||||
|
expect(parseCaseNumber(afis.caseNumber)?.prefix).toBe('AfiS')
|
||||||
|
})
|
||||||
|
|
||||||
it('gibt einer zu niedrigen Stufe nicht einmal die Existenz preis', async () => {
|
it('gibt einer zu niedrigen Stufe nicht einmal die Existenz preis', async () => {
|
||||||
const { createCase, loadVisibleCase } = await import('@/lib/cases/service')
|
const { createCase, loadVisibleCase } = await import('@/lib/cases/service')
|
||||||
const geheim = await createCase(perms(5, VOLL), {
|
const geheim = await createCase(perms(5, VOLL), {
|
||||||
|
|||||||
Reference in New Issue
Block a user