Files
AfiS-Aktensystem/web/app/(app)/faelle/[caseNumber]/aenderungen/page.tsx
T
Harlekin 399988596f Phase 3 abgeschlossen: Vorlagen, Abschnitte, Schwärzung, Diff
Eine Vorlage ist eine geordnete Liste von Bausteinen: Eingabefelder und feste
Textbausteine mit Platzhaltern. Damit trägt dieselbe Struktur den
Vorfallsbericht und den Sonderarbeitsvertrag. Die Startvorlagen bilden jetzt
den echten Bericht der Abteilung ab, samt Berichtsart, Gefahreneinschätzung
und Foundation-Freigabe — letztere als gewöhnliches Feld ohne Wirkung auf den
Zugriff.

Abschnitte werden gerendert und bearbeitet, Entwürfe alle zwei Sekunden lokal
gesichert. Gespeichert wird auf Knopfdruck: ein Abschnitt ist der Sachstand
der Akte, kein Chatbeitrag. Die geladene Version geht mit; weicht sie ab, gibt
es kein stilles Überschreiben, sondern beide Fassungen nebeneinander mit
"Meine übernehmen" und "Fremde übernehmen".

Felder über der Stufe des Lesers werden serverseitig durch einen Platzhalter
ersetzt, Schwärzungen im Fliesstext kommen als Segmente. Beides so, dass der
echte Wert den Server nicht verlässt — nicht als Ausblenden im Client.

Der Wortdiff ist selbst geschrieben statt eingekauft: ein Dutzend Zeilen gegen
30 kB im Bündel für etwas, das nur serverseitig läuft.

Beim Bauen des Vorlagen-Editors riss das Bündel-Budget auf 127,9 kB, weil zod
über das Vorlagenmodul in den Client geriet. Typen und Validierung sind jetzt
getrennt: fields.ts ohne zod für Client und Server, schema.ts nur
serverseitig. Wieder bei 111,9 kB von 120.

85 Tests, 22 E2E.
2026-09-02 12:52:53 +02:00

100 lines
3.7 KiB
TypeScript

import type { Metadata } from 'next'
import Link from 'next/link'
import { notFound } from 'next/navigation'
import { db } from '@/lib/db'
import { requireUser } from '@/lib/auth/current'
import { loadVisibleCase } from '@/lib/cases/service'
import { loadChangeHistory } from '@/lib/cases/entryService'
import { de } from '@/lib/i18n/de'
import { wordDiff } from '@/lib/diff'
export const metadata: Metadata = { title: de.entry.changesHeading }
export const dynamic = 'force-dynamic'
/**
* Änderungsübersicht als eigener Reiter an der Akte.
*
* Für die Abteilungsleitung ist das die Ansicht, mit der sie im Zweifel prüft,
* ob an einer Akte nachträglich gedreht wurde. Sichtbar für alle, die die Akte
* lesen dürfen — nicht nur für die Leitung. Das ist der ganze Sinn der Sache.
*/
export default async function ChangesPage({
params,
}: {
params: Promise<{ caseNumber: string }>
}) {
const user = await requireUser()
const { caseNumber } = await params
const akte = await loadVisibleCase(user.permissions, caseNumber)
if (!akte) notFound()
const versionen = await loadChangeHistory(user.permissions, akte.id)
const namen = new Map(
(
await db.user.findMany({
where: { id: { in: [...new Set(versionen.map((v) => v.editedById))] } },
select: { id: true, displayName: true },
})
).map((u) => [u.id, u.displayName]),
)
return (
<main className="mx-auto max-w-5xl px-6 py-10">
<p className="font-mono text-xs text-text-dim">
<Link href={`/faelle/${akte.caseNumber}`}>{akte.caseNumber}</Link>
</p>
<h1 className="mt-2 font-condensed">{de.entry.changesHeading}</h1>
<p className="mt-2 max-w-(--container-measure) text-text-dim">{de.entry.changesIntro}</p>
{versionen.length === 0 ? (
<p className="mt-8 text-text-dim">{de.entry.changesEmpty}</p>
) : (
<ol className="mt-8 border-t border-line">
{versionen.map((version) => (
<li key={version.id} className="border-b border-line py-5">
<div className="flex flex-wrap items-baseline gap-x-3 gap-y-1 text-2xs text-text-dim">
<span className="font-condensed">
{de.entry.versionLabel(version.version)} → {version.version + 1}
</span>
<span className="font-mono">
{namen.get(version.editedById) ?? version.editedById}
</span>
<time dateTime={version.editedAt.toISOString()}>
{version.editedAt.toLocaleString('de-DE')}
</time>
</div>
{version.changeReason ? (
<p className="mt-1.5 text-sm">
<span className="font-condensed text-text-dim">{de.entry.changeReason}: </span>
{version.changeReason}
</p>
) : null}
{/* Wortdiff statt zweier Spalten: bei langen Vermerken sieht man
sonst nicht, was sich geändert hat. */}
<p className="mt-3 max-w-(--container-measure) text-sm whitespace-pre-wrap">
{wordDiff(version.body, version.entry.body).map((teil, index) =>
teil.kind === 'gleich' ? (
<span key={index}>{teil.text}</span>
) : teil.kind === 'entfernt' ? (
<del key={index} className="text-stamp-4 decoration-stamp-4">
{teil.text}
</del>
) : (
<ins key={index} className="text-stamp-2 no-underline">
{teil.text}
</ins>
),
)}
</p>
</li>
))}
</ol>
)}
</main>
)
}