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
375 Vorkommen in 210 Dateien. Betrifft Kommentare, Oberflaechentexte und die README; an keiner Stelle Bedeutung, nur Zeichensetzung.
108 lines
4.2 KiB
TypeScript
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>
|
|
)
|
|
}
|