Files
AfiS-Aktensystem/web/app/(app)/personalakten/page.tsx
T
Harlekin 738330cee5 Vollständiger Statusfilter, Beteiligte beim Anlegen, Knöpfe in den Listen
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.
2026-09-03 23:38:56 +02:00

111 lines
3.9 KiB
TypeScript

import type { Metadata } from 'next'
import Link from 'next/link'
import { db } from '@/lib/db'
import { requirePermission } from '@/lib/auth/current'
import { listPersonnelFiles, listStatusTags } from '@/lib/personnel/service'
import { de } from '@/lib/i18n/de'
import { StampMark, classificationEdgeStyle } from '@/components/StampMark'
import { StatusTags } from '@/components/StatusTags'
export const metadata: Metadata = { title: de.personnel.listHeading }
export const dynamic = 'force-dynamic'
/**
* Liste der Personalakten.
*
* Wie bei den Fallakten: Suche, Sichtbarkeit und Zählung laufen über dieselbe
* Abfrage. Eine Akte über der Stufe des Lesers taucht weder auf noch in der
* Zahl daneben.
*/
export default async function PersonnelListPage({
searchParams,
}: {
searchParams: Promise<{ q?: string; status?: string }>
}) {
// Ohne das Recht gibt es die Seite nicht — 404, nicht 403.
const user = await requirePermission('personnel.read')
const { q, status } = await searchParams
const [{ files, total }, levels, statusTags] = await Promise.all([
listPersonnelFiles(user.permissions, { query: q, statusTag: status }),
db.clearanceLevelConfig.findMany({ orderBy: { level: 'asc' } }),
listStatusTags(),
])
const levelName = new Map(levels.map((l) => [l.level, l.name]))
const darfAnlegen =
user.permissions.isAdmin || user.permissions.permissions.has('personnel.edit')
return (
<main className="mx-auto max-w-5xl px-6 py-10">
<div className="flex flex-wrap items-baseline justify-between gap-4">
<h1 className="font-condensed">{de.personnel.listHeading}</h1>
<span className="record-key">{de.personnel.countLabel(total)}</span>
</div>
<form method="get" className="mt-4 flex flex-wrap items-center gap-3">
<input
type="search"
name="q"
defaultValue={q ?? ''}
placeholder={de.personnel.search}
aria-label={de.personnel.search}
className="w-full max-w-sm field"
/>
{status ? <input type="hidden" name="status" value={status} /> : null}
{darfAnlegen ? (
<Link href="/personalakten/neu" className="ml-auto font-condensed text-xs">
{de.personnel.create}
</Link>
) : null}
</form>
<nav className="mt-4 flex flex-wrap gap-2">
<Link
href={q ? `/personalakten?q=${encodeURIComponent(q)}` : '/personalakten'}
aria-current={status ? undefined : 'page'}
className="btn btn-sm"
>
{de.personnel.filterAll}
</Link>
{statusTags.map((tag) => (
<Link
key={tag.key}
href={`/personalakten?${new URLSearchParams({ ...(q ? { q } : {}), status: tag.key })}`}
aria-current={tag.key === status ? 'page' : undefined}
className="btn btn-sm"
>
{tag.name}
</Link>
))}
</nav>
{files.length === 0 ? (
<p className="mt-10 text-text-dim">
{q ? de.personnel.emptySearch : de.personnel.empty}
</p>
) : (
<ul className="mt-6 border-t border-line">
{files.map((akte) => (
<li key={akte.id} className="border-b border-line">
<Link
href={`/personalakten/${akte.id}`}
prefetch
className="record-row py-3 pl-3 text-text no-underline"
style={classificationEdgeStyle(akte.classification)}
>
<span className="record-title">{akte.displayName}</span>
<span className="record-meta">{akte.rank}</span>
<StatusTags tags={akte.statusTags} known={statusTags} />
<StampMark
level={akte.classification}
name={levelName.get(akte.classification) ?? de.common.unknown}
/>
</Link>
</li>
))}
</ul>
)}
</main>
)
}