From 738330cee53825a0f1a2f6410cc8f358932e9f0d Mon Sep 17 00:00:00 2001 From: Harlekin Date: Thu, 3 Sep 2026 23:38:56 +0200 Subject: [PATCH] =?UTF-8?q?Vollst=C3=A4ndiger=20Statusfilter,=20Beteiligte?= =?UTF-8?q?=20beim=20Anlegen,=20Kn=C3=B6pfe=20in=20den=20Listen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- web/app/(app)/abteilungen/[key]/page.tsx | 2 +- web/app/(app)/faelle/actions.ts | 20 ++-- web/app/(app)/faelle/neu/NewCaseForm.tsx | 122 ++++++++++++++++++----- web/app/(app)/faelle/neu/page.tsx | 11 +- web/app/(app)/faelle/page.tsx | 30 ++++-- web/app/(app)/personalakten/page.tsx | 11 +- web/app/(app)/uebersicht/page.tsx | 4 +- web/lib/cases/terms.ts | 56 +++++++++++ web/lib/i18n/cases.ts | 14 +++ web/tests/beteiligte-anlegen.test.ts | 67 +++++++++++++ 10 files changed, 295 insertions(+), 42 deletions(-) create mode 100644 web/tests/beteiligte-anlegen.test.ts diff --git a/web/app/(app)/abteilungen/[key]/page.tsx b/web/app/(app)/abteilungen/[key]/page.tsx index 2579fea..60b7246 100644 --- a/web/app/(app)/abteilungen/[key]/page.tsx +++ b/web/app/(app)/abteilungen/[key]/page.tsx @@ -60,7 +60,7 @@ export default async function DepartmentPage({ params }: { params: Promise<{ key
- {abteilung.key} + {abteilung.key} {de.department.one} {abteilung.isArchived ? ( diff --git a/web/app/(app)/faelle/actions.ts b/web/app/(app)/faelle/actions.ts index 9471939..ed97057 100644 --- a/web/app/(app)/faelle/actions.ts +++ b/web/app/(app)/faelle/actions.ts @@ -9,7 +9,7 @@ import { editEntry, restoreEntry, retractEntry } from '@/lib/cases/entryService' import { addParticipant, removeParticipant } from '@/lib/cases/participants' import { linkCases, unlinkCases } from '@/lib/cases/links' import { toggleWatch } from '@/lib/cases/watch' -import { LINK_KINDS, PARTICIPANT_ROLES } from '@/lib/cases/terms' +import { LINK_KINDS, PARTICIPANT_ROLES, parseParticipantDrafts } from '@/lib/cases/terms' import { de } from '@/lib/i18n/de' export type ActionResult = { ok: true } | { ok: false; error: string } @@ -46,22 +46,28 @@ export async function createCaseAction( title: formData.get('title'), classification: formData.get('classification'), templateKey: formData.get('templateKey'), - personnelFileId: formData.get('personnelFileId') || undefined, }) if (!parsed.success) return { error: de.actionErrors.default! } + // Beliebig viele Beteiligte. Was nicht sauber ankommt, fällt weg: die Akte + // soll nicht an einer krummen Zeile scheitern. Siehe lib/cases/terms.ts. + const beteiligte = parseParticipantDrafts(formData.getAll('participants')) + let caseNumber: string try { const created = await createCase(user.permissions, parsed.data) caseNumber = created.caseNumber - // Die betroffene Person gleich mitverknüpfen, damit sie nicht in einem - // zweiten Schritt vergessen wird. Scheitert das, steht die Akte trotzdem. - if (parsed.data.personnelFileId) { + // Die Beteiligten gleich mitverknüpfen, damit sie nicht in einem zweiten + // Schritt vergessen werden. Scheitert einer, steht die Akte trotzdem — sie + // ist das Wichtigere, und nachtragen kann man auf der Aktenseite. + for (const eintrag of beteiligte) { await addParticipant(user.permissions, { caseNumber, - role: 'SUBJECT', - personnelFileId: parsed.data.personnelFileId, + role: eintrag.role, + ...(eintrag.kind === 'user' + ? { userId: eintrag.id } + : { personnelFileId: eintrag.id }), }) } } catch (error) { diff --git a/web/app/(app)/faelle/neu/NewCaseForm.tsx b/web/app/(app)/faelle/neu/NewCaseForm.tsx index 3a0193e..1a4f292 100644 --- a/web/app/(app)/faelle/neu/NewCaseForm.tsx +++ b/web/app/(app)/faelle/neu/NewCaseForm.tsx @@ -1,23 +1,48 @@ 'use client' -import { useActionState } from 'react' -import { caseText } from '@/lib/i18n/cases' +import { useActionState, useState } from 'react' +import { caseText, participantText } from '@/lib/i18n/cases' import { clearanceText } from '@/lib/i18n/common' +import { PARTICIPANT_ROLES } from '@/lib/cases/terms' import { createCaseAction, type CreateState } from '../actions' +/** Ein Beteiligter im Formular, bevor die Akte existiert. */ +type Entwurf = { schluessel: number; ziel: string; rolle: string } + export function NewCaseForm({ templates, levels, people, + members, defaultLevel, }: { templates: { key: string; name: string }[] levels: { level: number; name: string }[] people: { id: string; displayName: string }[] + members: { id: string; displayName: string }[] defaultLevel: number }) { const [state, action, pending] = useActionState(createCaseAction, {}) + // Beliebig viele Beteiligte, jeder mit seiner Rolle. Vorher gab es genau + // eine „betroffene Person" — wer einen Zeugen und einen Täter eintragen + // wollte, musste die Akte erst anlegen und dann noch einmal hinein. + const [beteiligte, setBeteiligte] = useState([]) + const [naechster, setNaechster] = useState(1) + + function hinzufuegen() { + setBeteiligte((bisher) => [...bisher, { schluessel: naechster, ziel: '', rolle: 'SUBJECT' }]) + setNaechster((n) => n + 1) + } + + function aendern(schluessel: number, teil: Partial) { + setBeteiligte((bisher) => + bisher.map((eintrag) => + eintrag.schluessel === schluessel ? { ...eintrag, ...teil } : eintrag, + ), + ) + } + return (
@@ -71,28 +96,79 @@ export function NewCaseForm({
- {people.length > 0 ? ( -
- - {/* Gleich beim Anlegen verknüpfen erspart den zweiten Schritt, den - man sonst vergisst. Nachträglich geht es genauso. */} - + + + + ))} - -
- ) : null} + + )} + + +
diff --git a/web/app/(app)/faelle/page.tsx b/web/app/(app)/faelle/page.tsx index b9ae0f1..f5424d5 100644 --- a/web/app/(app)/faelle/page.tsx +++ b/web/app/(app)/faelle/page.tsx @@ -4,6 +4,7 @@ import { db } from '@/lib/db' import { requireUser } from '@/lib/auth/current' import { visibleCaseWhere } from '@/lib/access/caseFilter' import { de } from '@/lib/i18n/de' +import { CASE_STATUSES } from '@/lib/cases/terms' import { StampMark, classificationEdgeStyle } from '@/components/StampMark' export const metadata: Metadata = { title: de.case.listHeading } @@ -72,18 +73,35 @@ export default async function CaseListPage({

{de.case.listHeading}

- {de.case.countLabel(total)} + {de.case.countLabel(total)}
-