Phase 4 abgeschlossen: Personalakten, Dokumente, Beteiligte, Verknüpfungen
Die Dokumentebene aus docs/13 ist gebaut, additiv: Document, DocumentSection und DocumentSectionVersion neben Case, CaseSection und CaseEntry. Ein Dokument hängt an einer Fallakte, an einer Personalakte oder an nichts, höchstens an einem — gesichert durch eine CHECK-Bedingung. Es gibt kein DELETE; eine Zurücknahme lässt einen Grabstein stehen, der auch für den Admin zu ist. Eine neue Dokumentart ist damit ein Datensatz, kein Deployment. Im Seed stehen vier: vermerk, vorfallsbericht, sonderarbeitsvertrag (mehrseitiger Vertragstext mit Platzhaltern und Unterschriftenzeilen) und bussgeld. Zwei Umbenennungen, beide von Hand migriert, damit die vorhandenen Zeilen und Zählerstände stehen bleiben: - CaseTemplate -> DocumentTemplate, plus uses[] und layout - CaseCounter -> NumberSeries, ein Nummernkreis je Präfix für Aktenzeichen und Dokumentnummern Die Regeln für Abschnitte stehen jetzt einmal in lib/sections/render.ts und gelten für Fallakten und Dokumente gleichermaßen. Die Frage aus docs/13, ob CaseSection langfristig in die Dokumentebene wandert, ist damit verneint und begründet. Rückverweise auf der Personalakte laufen durch visibleCaseWhere, die Zahl daneben entsteht aus dem gefilterten Ergebnis. Auch die Rollengruppe verschwindet mit, sonst verriete "Betroffener" ohne Eintrag die Ermittlung. Die eigene Personalakte ist keine Ausnahme von der Freigabestufe. Vier neue Rechte: document.create, .edit, .finalize, .retract. Das letzte steht bei den zurückgehaltenen, es greift in Geschriebenes ein. 91 Tests im Web-Paket, 15 in der Datenbank, 28 e2e. Größte Route 118 kB gegen das Budget von 120 kB — die Luft wird knapp. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f5769ca58a
commit
d65cc95eb2
@@ -137,7 +137,11 @@ model Session {
|
||||
// Vorlagen
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
model CaseTemplate {
|
||||
/// Eine Vorlage beschreibt einen Bausteinsatz. Sie kann eine **Fallart** sein
|
||||
/// (bestimmt die Abschnitte einer Fallakte) oder eine **Dokumentart** (bestimmt
|
||||
/// den Aufbau eines Dokuments) oder beides. Was davon gilt, steht in `uses`.
|
||||
/// Eine neue Dokumentart ist damit ein Datensatz, kein Deployment.
|
||||
model DocumentTemplate {
|
||||
id String @id @default(uuid(7))
|
||||
key String // "korruptionsverdacht"
|
||||
version Int
|
||||
@@ -146,6 +150,13 @@ model CaseTemplate {
|
||||
isActive Boolean @default(true)
|
||||
fields Json // TemplateField[]
|
||||
|
||||
/// Wofuer die Vorlage verwendet werden darf. Liste statt Einzelwert, weil
|
||||
/// dieselbe Vorlage an mehreren Stellen sinnvoll sein kann.
|
||||
uses TemplateUse[] @default([CASE])
|
||||
|
||||
/// Bestimmt allein das Aussehen, nicht den Zugriff. Siehe docs/08.
|
||||
layout DocumentLayout @default(FORM)
|
||||
|
||||
/// Muster des Aktenzeichens, siehe docs/13-entscheidungen.md. Die Abteilung
|
||||
/// fuehrt gewachsene Praefixe wie "AKT-PA"; der Nummernkreis laeuft je
|
||||
/// Praefix und Jahr, damit zwei Fallarten sich nicht in die Quere kommen.
|
||||
@@ -154,12 +165,35 @@ model CaseTemplate {
|
||||
createdAt DateTime @default(now())
|
||||
createdById String?
|
||||
|
||||
cases Case[]
|
||||
cases Case[]
|
||||
documents Document[]
|
||||
|
||||
@@unique([key, version])
|
||||
@@index([key, isActive])
|
||||
}
|
||||
|
||||
/// Wofuer eine Vorlage taugt.
|
||||
enum TemplateUse {
|
||||
/// Fallart: bestimmt die Abschnitte einer Fallakte
|
||||
CASE
|
||||
/// Dokument in einer Fallakte
|
||||
CASE_DOCUMENT
|
||||
/// Dokument an einer Personalakte, etwa ein Sonderarbeitsvertrag
|
||||
PERSON_DOCUMENT
|
||||
/// Dokument ohne Besitzer, etwa ein Bussgeld
|
||||
STANDALONE
|
||||
}
|
||||
|
||||
/// Bauform des Dokuments, siehe docs/13-entscheidungen.md.
|
||||
enum DocumentLayout {
|
||||
/// beschriftete Felder, ausgefuellt — der Vorfallsbericht
|
||||
FORM
|
||||
/// fester Wortlaut mit Platzhaltern, mehrseitig — der Sonderarbeitsvertrag
|
||||
PROSE
|
||||
/// ein einzelnes Freitextfeld, dargestellt wie ein Vermerk
|
||||
NOTE
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fallakten
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -183,7 +217,7 @@ model Case {
|
||||
/// B=Abschnitte, C=Verlaufseintraege.
|
||||
searchVector Unsupported("tsvector")?
|
||||
|
||||
template CaseTemplate @relation(fields: [templateId], references: [id])
|
||||
template DocumentTemplate @relation(fields: [templateId], references: [id])
|
||||
sections CaseSection[]
|
||||
entries CaseEntry[]
|
||||
participants CaseParticipant[]
|
||||
@@ -193,6 +227,7 @@ model Case {
|
||||
watches Watch[]
|
||||
linksFrom CaseLink[] @relation("from")
|
||||
linksTo CaseLink[] @relation("to")
|
||||
documents Document[]
|
||||
|
||||
@@index([status, updatedAt(sort: Desc)])
|
||||
@@index([classification])
|
||||
@@ -215,9 +250,10 @@ enum CaseStatus {
|
||||
SEALED
|
||||
}
|
||||
|
||||
/// Kollisionsfreie Vergabe des Aktenzeichens, ein Nummernkreis je Praefix und
|
||||
/// Jahr. Hochgezaehlt in derselben Transaktion wie das Anlegen des Falls.
|
||||
model CaseCounter {
|
||||
/// Kollisionsfreie Vergabe der Geschaeftsnummern, ein Nummernkreis je Praefix
|
||||
/// und Jahr. Hochgezaehlt in derselben Transaktion wie das Anlegen. Traegt
|
||||
/// Aktenzeichen und Dokumentnummern, das Praefix kommt aus der Vorlage.
|
||||
model NumberSeries {
|
||||
prefix String
|
||||
year Int
|
||||
lastNumber Int @default(0)
|
||||
@@ -378,11 +414,14 @@ model CaseExclusion {
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
/// Verknuepfung zweier Akten. Umkehrbar loeschbar, siehe docs/09: kein
|
||||
/// Bestaetigungsdialog, sondern ein Rueckgaengig-Hinweis.
|
||||
model CaseLink {
|
||||
fromId String
|
||||
toId String
|
||||
kind LinkKind
|
||||
createdAt DateTime @default(now())
|
||||
fromId String
|
||||
toId String
|
||||
kind LinkKind
|
||||
createdById String?
|
||||
createdAt DateTime @default(now())
|
||||
from Case @relation("from", fields: [fromId], references: [id], onDelete: Cascade)
|
||||
to Case @relation("to", fields: [toId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@ -410,14 +449,18 @@ model PersonnelFile {
|
||||
status String?
|
||||
classification Int @default(1)
|
||||
fields Json @default("{}")
|
||||
/// Freitext im Kopf der Akte, etwa "kein Discord-Konto, Aktenlage aus 2025".
|
||||
note String?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
createdById String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
searchVector Unsupported("tsvector")?
|
||||
|
||||
user User? @relation(fields: [userId], references: [id])
|
||||
participants CaseParticipant[]
|
||||
documents Document[]
|
||||
|
||||
@@index([displayName])
|
||||
@@index([userId])
|
||||
@@ -426,6 +469,102 @@ model PersonnelFile {
|
||||
@@index([displayName(ops: raw("gin_trgm_ops"))], map: "personnel_name_trgm_idx", type: Gin)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Dokumente
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Eine Ausfertigung aus einer Vorlage: Vorfallsbericht, Sonderarbeitsvertrag,
|
||||
/// spaeter Bussgeld. Besitzer ist eine Fallakte, eine Personalakte oder nichts.
|
||||
/// Hoechstens einer der beiden Fremdschluessel ist gesetzt, gesichert durch
|
||||
/// eine CHECK-Bedingung in der Migration.
|
||||
///
|
||||
/// Anhaengend wie ein Verlaufseintrag: es gibt kein DELETE, eine Zuruecknahme
|
||||
/// laesst einen Grabstein stehen. Siehe CLAUDE.md, harte Regel 2.
|
||||
model Document {
|
||||
id String @id @default(uuid(7))
|
||||
documentNumber String @unique // SAV-2026-0007
|
||||
title String
|
||||
templateId String
|
||||
templateKey String
|
||||
templateVersion Int
|
||||
/// Kopie aus der Vorlage: das Aussehen darf sich nicht aendern, wenn jemand
|
||||
/// spaeter die Vorlage umbaut.
|
||||
layout DocumentLayout @default(FORM)
|
||||
status DocumentStatus @default(DRAFT)
|
||||
classification Int @default(1)
|
||||
|
||||
caseId String?
|
||||
personnelFileId String?
|
||||
|
||||
createdById String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
finalizedAt DateTime?
|
||||
finalizedById String?
|
||||
|
||||
retractedAt DateTime?
|
||||
retractedById String?
|
||||
retractionReason String?
|
||||
|
||||
/// Von einem Trigger gepflegt. Gewichtung A=Nummer/Titel, B=Abschnitte.
|
||||
searchVector Unsupported("tsvector")?
|
||||
|
||||
template DocumentTemplate @relation(fields: [templateId], references: [id])
|
||||
case Case? @relation(fields: [caseId], references: [id], onDelete: Restrict)
|
||||
personnelFile PersonnelFile? @relation(fields: [personnelFileId], references: [id], onDelete: Restrict)
|
||||
sections DocumentSection[]
|
||||
|
||||
@@index([caseId, createdAt(sort: Desc)])
|
||||
@@index([personnelFileId, createdAt(sort: Desc)])
|
||||
@@index([classification])
|
||||
@@index([templateKey])
|
||||
@@index([createdById])
|
||||
// Von Hand in der Migration erzeugt, Prisma muss sie kennen. Siehe Case.
|
||||
@@index([searchVector], map: "document_search_idx", type: Gin)
|
||||
@@index([documentNumber(ops: raw("gin_trgm_ops"))], map: "document_number_trgm_idx", type: Gin)
|
||||
}
|
||||
|
||||
enum DocumentStatus {
|
||||
/// in Arbeit, aenderbar
|
||||
DRAFT
|
||||
/// ausgefertigt, Aenderungen brauchen ein eigenes Recht
|
||||
FINAL
|
||||
/// zurueckgezogen, bleibt als Grabstein stehen
|
||||
RETRACTED
|
||||
}
|
||||
|
||||
/// Ein ausgefuelltes Feld eines Dokuments. Dieselben Zusicherungen wie bei
|
||||
/// `CaseSection`: versioniert, `minClearance` je Feld, Diff, Protokoll.
|
||||
model DocumentSection {
|
||||
id String @id @default(uuid(7))
|
||||
documentId String
|
||||
fieldKey String
|
||||
minClearance Int @default(0)
|
||||
currentVersion Int @default(1)
|
||||
value Json
|
||||
updatedAt DateTime @updatedAt
|
||||
updatedById String
|
||||
|
||||
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
|
||||
versions DocumentSectionVersion[]
|
||||
|
||||
@@unique([documentId, fieldKey])
|
||||
}
|
||||
|
||||
model DocumentSectionVersion {
|
||||
id String @id @default(uuid(7))
|
||||
sectionId String
|
||||
version Int
|
||||
value Json
|
||||
changeReason String?
|
||||
authorId String
|
||||
createdAt DateTime @default(now())
|
||||
section DocumentSection @relation(fields: [sectionId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([sectionId, version])
|
||||
@@index([sectionId, version(sort: Desc)])
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Dateien
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user