Files
AfiS-Aktensystem/web/app/(app)/faelle/[caseNumber]/layout.tsx
T
Harlekin e0b03ea336
Prüfung / Lint, Typen, Tests (push) Canceled after 0s
Prüfung / Images bauen (pdf) (push) Canceled after 0s
Prüfung / Images bauen (web) (push) Canceled after 0s
Prüfung / Images bauen (worker) (push) Canceled after 0s
Images / bauen (pdf, docker/pdf.Dockerfile) (push) Canceled after 0s
Images / bauen (web, docker/web.Dockerfile) (push) Canceled after 0s
Images / bauen (worker, docker/worker.Dockerfile) (push) Canceled after 0s
Gedankenstriche durch Bindestriche ersetzt
375 Vorkommen in 210 Dateien. Betrifft Kommentare, Oberflaechentexte und die
README; an keiner Stelle Bedeutung, nur Zeichensetzung.
2026-09-04 08:54:52 +02:00

108 lines
4.2 KiB
TypeScript

import type { ReactNode } from 'react'
import { notFound } from 'next/navigation'
import { db } from '@/lib/db'
import { Tabs } from '@/components/Tabs'
import { requireUser } from '@/lib/auth/current'
import { loadVisibleCase } from '@/lib/cases/service'
import { de } from '@/lib/i18n/de'
import { StampMark, classificationEdgeStyle } from '@/components/StampMark'
import { allowedTargets, type CaseStatusName } from '@/lib/cases/lifecycle'
import { CaseActions } from './CaseActions'
import { WatchToggle } from './WatchToggle'
import { isWatching } from '@/lib/cases/watch'
import { trackOpen } from '@/lib/recent/track'
export const dynamic = 'force-dynamic'
/**
* Kopf und Reiter einer Fallakte.
*
* Die Akte ist in Reiter geteilt, und zwar in **eigene Seiten**, nicht in
* umgeschaltete Blöcke: nur so wird der Code getrennt geladen. Die Fallakte
* war die dichteste Seite der Anwendung und hatte das Bündelbudget aus
* docs/09-performance-und-ux.md aufgebraucht - Beteiligte, Verknüpfungen,
* Dokumente und Anhänge lagen mit auf der Seite, obwohl man sie selten
* gleichzeitig braucht.
*
* Fachlich passt der Schnitt ohnehin: die drei Ebenen aus
* docs/05-akten-und-vorlagen.md - Kopfdaten, Abschnitte, Verlauf - bleiben
* zusammen, das Umfeld der Akte bekommt seinen eigenen Platz.
*/
export default async function CaseLayout({
children,
params,
}: {
children: ReactNode
params: Promise<{ caseNumber: string }>
}) {
const user = await requireUser()
const { caseNumber } = await params
const akte = await loadVisibleCase(user.permissions, caseNumber)
// Kein Zugriff heisst 404, nicht 403: die Existenz ist selbst eine Information.
if (!akte) notFound()
// Für die eigene Sprungliste vormerken. Steht im Layout und nicht auf einem
// Reiter: wer die Beteiligten aufruft, hat die Akte geöffnet.
await trackOpen(user.id, 'CASE', akte.id)
const [levels, beobachtet] = await Promise.all([
db.clearanceLevelConfig.findMany({ orderBy: { level: 'asc' } }),
isWatching(user.permissions, akte.id),
])
const level = levels.find((l) => l.level === akte.classification)
const basis = `/faelle/${akte.caseNumber}`
const reiter = [
{ href: basis, label: de.case.tabFile },
{ href: `${basis}/beteiligte`, label: de.case.tabRelations },
{ href: `${basis}/dokumente`, label: de.case.tabDocuments },
{ href: `${basis}/aenderungen`, label: de.case.changes },
{ href: `${basis}/export`, label: de.export.heading },
]
return (
<main className="mx-auto max-w-5xl px-6 py-10">
<header className="pl-3" style={classificationEdgeStyle(akte.classification)}>
<div className="flex flex-wrap items-baseline gap-x-4 gap-y-2">
<span className="font-mono text-xs text-text-dim">{akte.caseNumber}</span>
<StampMark level={akte.classification} name={level?.name ?? de.common.unknown} />
<WatchToggle caseNumber={akte.caseNumber} initial={beobachtet} />
</div>
<h1 className="mt-2 font-condensed">{akte.title}</h1>
<dl className="mt-3 flex flex-wrap gap-x-8 gap-y-1 text-xs text-text-dim">
<div className="flex gap-2">
<dt className="font-condensed">{de.case.status}</dt>
<dd>{de.case.statusNames[akte.status]}</dd>
</div>
<div className="flex gap-2">
<dt className="font-condensed">{de.case.type}</dt>
<dd>{akte.templateKey}</dd>
</div>
{akte.dueAt ? (
<div className="flex gap-2">
<dt className="font-condensed">{de.case.dueAt}</dt>
<dd>{akte.dueAt.toLocaleDateString('de-DE')}</dd>
</div>
) : null}
</dl>
{/* Statuswechsel und Umklassifizierung gehören zum Kopf und stehen
deshalb auf jedem Reiter zur Verfügung. */}
<CaseActions
caseNumber={akte.caseNumber}
classification={akte.classification}
statusTargets={allowedTargets(akte.status as CaseStatusName)}
levels={levels.map((l) => ({ level: l.level, name: l.name }))}
maxLevel={user.permissions.clearanceLevel}
dueAt={akte.dueAt ? akte.dueAt.toISOString().slice(0, 10) : ''}
/>
</header>
<Tabs tabs={reiter} />
<div className="mt-8">{children}</div>
</main>
)
}