Files
AfiS-Aktensystem/web/lib/cases/terms.ts
T
Harlekin 738330cee5 Vollständiger Statusfilter, Beteiligte beim Anlegen, Knöpfe in den Listen
Vier Sachen aus der Durchsicht.

**Der Statusfilter zeigte fünf von sieben Zuständen.** DRAFT und SEALED
fehlten - ausgerechnet die beiden, die in der Liste am häufigsten standen; man
konnte nach dem, was man sah, gerade nicht filtern. Das erklärt auch die Frage,
was „In Prüfung" und „Ruhend" bedeuten: die Zustände standen nirgends in der
Oberfläche. Jetzt alle sieben in der Reihenfolge des Lebenszyklus, mit einer
Erklärung als title, und Versiegelt nur für den, der Versiegeltes lesen darf.
Ein Test verlangt zu jedem Zustand einen Namen und eine Erklärung.

**Beliebig viele Beteiligte beim Anlegen.** Das Formular kannte genau eine
„betroffene Person", und die Rolle war auf SUBJECT festgelegt. Jetzt beliebig
viele Zeilen mit je einer Rolle, Konten und Personalakten in einer Auswahl, wie
auf der Aktenseite. Die Zerlegung steht als parseParticipantDrafts in
lib/cases/terms.ts und nicht in der Server Action: seit die Rolle über den
Zugriff entscheidet, darf eine erfundene Rolle nicht durchrutschen.

**Die Filterleisten sind Knöpfe** wie überall sonst, mit gefülltem aktivem
Filter.

**Keine leere Kennungsspalte mehr.** In „Zuletzt geöffnet" stand bei einer
Person nichts, wo sonst das Aktenzeichen steht - jetzt steht dort
„Personalakte". Nebenbei zwei Stellen zurückgenommen, an denen die
Spaltenklasse im Seitenkopf gelandet war statt in einer Zeile.
2026-09-03 23:38:56 +02:00

116 lines
3.9 KiB
TypeScript

/**
* Feste Wertelisten der Akte — Rollen der Beteiligten und Arten der
* Verknüpfung.
*
* **Ohne Abhängigkeiten.** Diese Datei wird auch im Client geladen, und die
* Dienstmodule daneben ziehen den Datenbankclient nach sich. Denselben Schnitt
* hat `lib/templates/fields.ts` gegenüber `schema.ts`, aus demselben Grund:
* das Bündelbudget aus docs/09-performance-und-ux.md.
*
* Die Übersetzungen stehen in `lib/i18n/de.ts`, nicht hier: diese Namen sind
* Datenbankwerte.
*/
export const PARTICIPANT_ROLES = [
'LEAD',
'INVESTIGATOR',
'OBSERVER',
'WITNESS',
'SUBJECT',
'SUSPECT',
'ACCOMPLICE',
] as const
export type ParticipantRoleName = (typeof PARTICIPANT_ROLES)[number]
export function isParticipantRole(value: string): value is ParticipantRoleName {
return (PARTICIPANT_ROLES as readonly string[]).includes(value)
}
/**
* Wer an der Akte **arbeitet** — und nur diese drei Rollen öffnen sie auch.
*
* Die Beteiligtenliste beantwortet zwei ganz verschiedene Fragen: *wer bearbeitet
* diesen Fall* und *um wen geht es darin*. Sie standen bisher gleichberechtigt
* nebeneinander, und weil ein Beteiligter die Akte lesen darf, hätte ein
* Betroffener mit Discord-Konto die Ermittlung gegen sich selbst mitlesen
* können. Mit „Täter" und „Mittäter" wäre das offensichtlich falsch geworden,
* es war es vorher schon.
*
* Deshalb: **nur Arbeitsrollen öffnen die Akte.** Zeuge, Betroffener, Täter und
* Mittäter sind Angaben über den Fall, keine Berechtigung an ihm.
* Siehe docs/03-auth-und-rechte.md.
*/
export const WORKING_ROLES = ['LEAD', 'INVESTIGATOR', 'OBSERVER'] as const
export type WorkingRoleName = (typeof WORKING_ROLES)[number]
/** Ob diese Rolle Zugang zur Akte verschafft. */
export function grantsCaseAccess(role: string): boolean {
return (WORKING_ROLES as readonly string[]).includes(role)
}
/**
* Die Zustände einer Fallakte in der Reihenfolge des Lebenszyklus aus
* docs/05-akten-und-vorlagen.md. `SEALED` steht am Ende, weil es aus jedem
* Zustand erreichbar ist und keine Stufe im Ablauf bildet.
*/
export const CASE_STATUSES = [
'DRAFT',
'OPEN',
'REVIEW',
'DORMANT',
'CLOSED',
'ARCHIVED',
'SEALED',
] as const
export const LINK_KINDS = ['RELATED', 'SUCCESSOR', 'MERGED_INTO'] as const
export type LinkKindName = (typeof LINK_KINDS)[number]
export function isLinkKind(value: string): value is LinkKindName {
return (LINK_KINDS as readonly string[]).includes(value)
}
/**
* Ein Beteiligter, wie ihn das Formular „Neue Fallakte" mitschickt.
*
* Je Zeile ein verstecktes Feld der Form `user:<id>|ROLLE` oder
* `file:<id>|ROLLE`. Die Zerlegung steht hier und nicht in der Server Action,
* damit sie prüfbar ist: sie entscheidet, wer mit welcher Rolle in eine Akte
* kommt, und eine unbekannte Rolle darf nicht durchrutschen.
*/
export type ParticipantDraft =
| { kind: 'user'; id: string; role: ParticipantRoleName }
| { kind: 'file'; id: string; role: ParticipantRoleName }
/** So viele Beteiligte nimmt das Formular höchstens an. */
export const MAX_PARTICIPANT_DRAFTS = 50
export function parseParticipantDrafts(werte: readonly unknown[]): ParticipantDraft[] {
const gesehen = new Set<string>()
const ergebnis: ParticipantDraft[] = []
for (const wert of werte) {
if (typeof wert !== 'string') continue
const [ziel, rolle] = wert.split('|')
const [art, id] = (ziel ?? '').split(':')
if (art !== 'user' && art !== 'file') continue
if (!id) continue
if (!rolle || !isParticipantRole(rolle)) continue
// Dieselbe Person in derselben Rolle nur einmal. In einer anderen Rolle
// darf sie durchaus zweimal stehen: Zeuge und Betroffener zugleich kommt vor.
const schluessel = `${art}:${id}|${rolle}`
if (gesehen.has(schluessel)) continue
gesehen.add(schluessel)
ergebnis.push({ kind: art, id, role: rolle })
if (ergebnis.length >= MAX_PARTICIPANT_DRAFTS) break
}
return ergebnis
}