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.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Harlekin
2026-09-02 12:10:34 +02:00
co-authored by Claude Opus 5
parent 87c74b251d
commit 5cbdabf337
7 changed files with 260 additions and 29 deletions
+36 -19
View File
@@ -1,40 +1,57 @@
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')}`
/**
* Aktenzeichen. Das Muster gehört zur Vorlage, nicht ins Programm: die
* 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())
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. */
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
* 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.
* `INSERT … ON CONFLICT DO UPDATE` sperrt die Zeile für die Dauer der
* Transaktion. Zwei gleichzeitige Anlagen warten dadurch aufeinander, statt
* dieselbe Nummer zu ziehen. Läuft in **derselben** Transaktion wie das
* Anlegen des Falls, siehe docs/04-datenmodell.md.
*/
export async function nextCaseNumber(
tx: Prisma.TransactionClient,
pattern: NumberPattern = DEFAULT_PATTERN,
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
INSERT INTO "CaseCounter" ("prefix", "year", "lastNumber")
VALUES (${pattern.prefix}, ${year}, 1)
ON CONFLICT ("prefix", "year") DO UPDATE
SET "lastNumber" = "CaseCounter"."lastNumber" + 1
RETURNING "lastNumber"
`
@@ -44,5 +61,5 @@ export async function nextCaseNumber(
throw new Error('Aktenzeichen konnte nicht vergeben werden')
}
return formatCaseNumber(year, lastNumber)
return formatCaseNumber(year, lastNumber, pattern)
}