Phase 2, erster Teil: Akten anlegen, lesen, Vermerke schreiben

Fachlogik in lib/cases/service.ts, Zugriffsprüfung in lib/access/. Jede
Funktion prüft selbst und schreibt ihren Protokolleintrag in derselben
Transaktion wie die Änderung — die Server Actions daneben sind dünn.

Schreibgrenzen nach docs/03: niemand legt eine Akte über der eigenen Stufe an,
niemand stuft über die eigene Stufe hoch, archiviert und versiegelt sind
schreibgeschützt auch für Admins. Eine Herabstufung verlangt eine Begründung
und landet als sichtbarer Systemeintrag im Verlauf, nicht nur im Protokoll.

Aktenzeichen über CaseCounter mit INSERT ... ON CONFLICT DO UPDATE, geprüft mit
acht gleichzeitigen Anlagen ohne Doppelung. Wer eine Akte anlegt, wird ihr
Ermittler, sonst verliert er sie aus dem Blick, sobald jemand hochstuft.

Fallliste cursorbasiert auf (updatedAt, id), Sichtbarkeit und Trefferzahl in
derselben Abfrage. Aktenansicht mit Kopfdaten sofort und Verlauf in eigener
Suspense-Grenze. Vermerke optimistisch mit clientseitiger UUIDv7, die zugleich
Idempotenzschlüssel ist — zweimal gesendet bleibt ein Eintrag.

Lesezugriffe ab Stufe 3 werden protokolliert, gepuffert und alle zwei Sekunden
gebündelt geschrieben, damit das Protokoll die Antwort nicht ausbremst.

Die vier Startvorlagen stehen jetzt im Seed statt in Phase 3: ohne Vorlage
lässt sich keine Akte anlegen.
This commit is contained in:
Harlekin
2026-09-01 22:57:38 +02:00
parent 31ac436fde
commit 651f239c68
18 changed files with 1587 additions and 4 deletions
+48
View File
@@ -0,0 +1,48 @@
import type { Prisma } from '@afis/db'
/** `AfiS-2026-0042`, siehe Glossar in docs/01-produkt.md. */
export function formatCaseNumber(year: number, number: number): string {
return `AfiS-${year}-${String(number).padStart(4, '0')}`
}
const PATTERN = /^AfiS-(\d{4})-(\d{4})$/
export function parseCaseNumber(value: string): { year: number; number: number } | null {
const match = PATTERN.exec(value.trim())
if (!match) return null
return { year: Number(match[1]), number: Number(match[2]) }
}
/** Erkennt Aktenzeichen im Fliesstext, für die Vorschaukarten im Chat. */
export const CASE_NUMBER_GLOBAL = /AfiS-\d{4}-\d{4}/g
/**
* Vergibt das nächste Aktenzeichen des Jahres.
*
* Läuft mit `SELECT … FOR UPDATE` in **derselben** Transaktion wie das Anlegen
* des Falls. Zwei gleichzeitige Anlagen warten dadurch aufeinander, statt
* dieselbe Nummer zu ziehen oder eine Lücke zu hinterlassen.
* 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(
tx: Prisma.TransactionClient,
year = new Date().getFullYear(),
): Promise<string> {
const rows = await tx.$queryRaw<{ lastNumber: number }[]>`
INSERT INTO "CaseCounter" ("year", "lastNumber")
VALUES (${year}, 1)
ON CONFLICT ("year") DO UPDATE
SET "lastNumber" = "CaseCounter"."lastNumber" + 1
RETURNING "lastNumber"
`
const lastNumber = rows[0]?.lastNumber
if (lastNumber === undefined) {
throw new Error('Aktenzeichen konnte nicht vergeben werden')
}
return formatCaseNumber(year, lastNumber)
}