import type { Metadata } from 'next' import { Suspense } from 'react' import { notFound } from 'next/navigation' import { db } from '@/lib/db' import { requireUser } from '@/lib/auth/current' import { visibleEntryWhere } from '@/lib/access/caseFilter' import { loadVisibleCase } from '@/lib/cases/service' import { recordRead, READ_AUDIT_FROM_LEVEL } from '@/lib/audit' import { de } from '@/lib/i18n/de' import { StampMark, classificationEdgeStyle } from '@/components/StampMark' import { SkeletonLine } from '@/components/Skeleton' import { EntryComposer } from './EntryComposer' import { CaseActions } from './CaseActions' import { EntryActions } from './EntryActions' import { allowedTargets, type CaseStatusName } from '@/lib/cases/lifecycle' import { evaluateEntryEdit, canRetract } from '@/lib/cases/entryPolicy' import Link from 'next/link' import type { EffectivePermissions } from '@/lib/auth/permissions' export const dynamic = 'force-dynamic' export async function generateMetadata({ params, }: { params: Promise<{ caseNumber: string }> }): Promise { const { caseNumber } = await params return { title: caseNumber } } /** * Der Verlauf hängt in einer eigenen Suspense-Grenze und streamt nach. Die * Kopfdaten stehen sofort, der Nutzer liest schon, während der Rest lädt. * Siehe docs/09-performance-und-ux.md. */ async function Timeline({ caseId, caseNumber, permissions, }: { caseId: string caseNumber: string permissions: EffectivePermissions }) { const settings = await db.appSetting.findUnique({ where: { id: 1 } }) const policy = { entryEditPolicy: (settings?.entryEditPolicy ?? 'GRACE_WINDOW') as | 'APPEND_ONLY' | 'GRACE_WINDOW' | 'OPEN', entryEditGraceMinutes: settings?.entryEditGraceMinutes ?? 15, entryEditMarkGrace: settings?.entryEditMarkGrace ?? false, } const entries = await db.caseEntry.findMany({ where: { AND: [visibleEntryWhere(permissions), { caseId }] }, orderBy: [{ createdAt: 'desc' }, { id: 'desc' }], take: 100, select: { id: true, type: true, body: true, minClearance: true, authorId: true, createdAt: true, version: true, editedAt: true, editVisible: true, retractedAt: true, retractionReason: true, }, }) if (entries.length === 0) { return

{de.entry.empty}

} const authors = await db.user.findMany({ where: { id: { in: [...new Set(entries.map((e) => e.authorId))] } }, select: { id: true, displayName: true }, }) const nameOf = new Map(authors.map((a) => [a.id, a.displayName])) return (
    {entries.map((entry) => (
  1. {de.entry.types[entry.type]} {nameOf.get(entry.authorId) ?? entry.authorId} {entry.editVisible && entry.editedAt ? ( {de.entry.edited(entry.version)} ) : null}
    {entry.retractedAt ? (

    {de.entry.retracted( entry.retractedAt.toLocaleDateString('de-DE'), nameOf.get(entry.authorId) ?? entry.authorId, entry.retractionReason ?? '', )}

    ) : (

    {entry.body}

    )}
  2. ))}
) } export default async function CasePage({ params, }: { 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() // Harte Regel 3: jeder Lesezugriff ab Stufe 3 wird protokolliert. if (akte.classification >= READ_AUDIT_FROM_LEVEL) { recordRead({ actorId: user.id, action: 'case.read', targetType: 'Case', targetId: akte.id, clearanceAtTime: user.permissions.clearanceLevel, meta: { caseNumber: akte.caseNumber, classification: akte.classification }, }) } const levels = await db.clearanceLevelConfig.findMany({ orderBy: { level: 'asc' } }) const level = levels.find((l) => l.level === akte.classification) return (
{akte.caseNumber}

{akte.title}

{de.case.status}
{de.case.statusNames[akte.status]}
{de.case.type}
{akte.templateKey}
{akte.dueAt ? (
{de.case.dueAt}
{akte.dueAt.toLocaleDateString('de-DE')}
) : null}
({ level: l.level, name: l.name }))} maxLevel={user.permissions.clearanceLevel} />
} >
) }