feat(protokoll): Protokoll ansehen, mit Filtern und Mandantensicht (#45)
This commit was merged in pull request #45.
This commit is contained in:
@@ -14,6 +14,7 @@ const REITER = [
|
||||
{ pfad: "/admin/benutzer", beschriftung: "Benutzer" },
|
||||
{ pfad: "/admin/abteilungen", beschriftung: "Abteilungen" },
|
||||
{ pfad: "/admin/schnellstart", beschriftung: "Schnellstart" },
|
||||
{ pfad: "/admin/protokoll", beschriftung: "Protokoll" },
|
||||
/*
|
||||
* Das Verzeichnis gibt es nur im Modus INTERN: dort hat das Haus **ein** AD,
|
||||
* aus dem seine Leute kommen. Im Modus DIENSTLEISTUNG waere es ein LDAP zu
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import Link from "next/link";
|
||||
import { ScrollText } from "lucide-react";
|
||||
import { ProtokollFilter, type ProtokollFilterfelder } from "@/components/protokoll/protokoll-filter";
|
||||
import { ProtokollListe } from "@/components/protokoll/protokoll-liste";
|
||||
import { sitzungErforderlich } from "@/server/auth/sitzungspflicht";
|
||||
import { kundenEbeneSichtbar } from "@/server/mandant";
|
||||
import { PRO_SEITE, protokollSeite, protokollUrheber } from "@/server/queries/protokoll";
|
||||
|
||||
/**
|
||||
* Das Protokoll ansehen (#023).
|
||||
*
|
||||
* Geschrieben wurde es seit #006 — Rollenänderungen, veröffentlichte Vorlagen,
|
||||
* archivierte und zurückgeholte Läufe, der Moduswechsel. Ansehen konnte es
|
||||
* niemand: wer wissen wollte, wer wann eine Rolle geändert hat, brauchte `psql`.
|
||||
*
|
||||
* `verwaltung` und nicht `lesen`: das Protokoll sagt, wer was getan hat, und ist
|
||||
* damit selbst eine Auskunft über Menschen.
|
||||
*
|
||||
* **Die Seite prüft selbst.** Das Admin-Layout darüber ist das zweite Netz und
|
||||
* nicht das erste (siehe CLAUDE.md).
|
||||
*/
|
||||
|
||||
export default async function ProtokollSeiteAnsicht({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
||||
}) {
|
||||
const { benutzer } = await sitzungErforderlich("verwaltung", "/admin/protokoll");
|
||||
const parameter = await searchParams;
|
||||
const einzeln = (name: string): string =>
|
||||
(Array.isArray(parameter[name]) ? parameter[name][0] : parameter[name])?.trim() ?? "";
|
||||
|
||||
const felder: ProtokollFilterfelder = {
|
||||
suche: einzeln("suche"),
|
||||
urheber: einzeln("urheber"),
|
||||
von: einzeln("von"),
|
||||
bis: einzeln("bis"),
|
||||
};
|
||||
const gewuenschteSeite = Number.parseInt(einzeln("seite"), 10);
|
||||
|
||||
const [ergebnis, urheber, kundenEbene] = await Promise.all([
|
||||
protokollSeite(
|
||||
{
|
||||
suche: felder.suche,
|
||||
...(felder.urheber ? { urheberId: felder.urheber } : {}),
|
||||
...(felder.von ? { vonDatum: felder.von } : {}),
|
||||
...(felder.bis ? { bisDatum: felder.bis } : {}),
|
||||
seite: Number.isNaN(gewuenschteSeite) ? 1 : gewuenschteSeite,
|
||||
},
|
||||
benutzer,
|
||||
),
|
||||
protokollUrheber(benutzer),
|
||||
kundenEbeneSichtbar(),
|
||||
]);
|
||||
|
||||
/** Dieselben Filter mitnehmen, nur die Seite wechseln. */
|
||||
function blaetternZu(seite: number): string {
|
||||
const suchteil = new URLSearchParams();
|
||||
if (felder.suche) suchteil.set("suche", felder.suche);
|
||||
if (felder.urheber) suchteil.set("urheber", felder.urheber);
|
||||
if (felder.von) suchteil.set("von", felder.von);
|
||||
if (felder.bis) suchteil.set("bis", felder.bis);
|
||||
suchteil.set("seite", String(seite));
|
||||
return `/admin/protokoll?${suchteil.toString()}`;
|
||||
}
|
||||
|
||||
const erster = (ergebnis.seite - 1) * PRO_SEITE + 1;
|
||||
const letzter = Math.min(ergebnis.seite * PRO_SEITE, ergebnis.gesamt);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className="flex items-center gap-2 text-2xl font-bold">
|
||||
<ScrollText className="size-6 text-nebentext" strokeWidth={1.75} aria-hidden="true" />
|
||||
Protokoll
|
||||
</h1>
|
||||
<p className="mt-2 max-w-prose text-nebentext">
|
||||
Wer hat was wann getan. Festgehalten wird, was nachvollziehbar bleiben muss — Rollenänderungen,
|
||||
veröffentlichte Vorlagen, archivierte und zurückgeholte Läufe, Verzeichnisquellen, der Moduswechsel.
|
||||
Einträge werden nie geändert und nie gelöscht.
|
||||
</p>
|
||||
|
||||
<ProtokollFilter felder={felder} urheber={urheber} />
|
||||
|
||||
<p className="mt-4 text-sm text-nebentext">
|
||||
{ergebnis.gesamt === 0 ? "Keine Einträge." : `${erster}–${letzter} von ${ergebnis.gesamt} Einträgen`}
|
||||
</p>
|
||||
|
||||
<ProtokollListe zeilen={ergebnis.zeilen} kundenEbene={kundenEbene} />
|
||||
|
||||
{ergebnis.seiten > 1 ? (
|
||||
<nav aria-label="Seiten" className="mt-6 flex flex-wrap items-center gap-3">
|
||||
{ergebnis.seite > 1 ? (
|
||||
<Link
|
||||
href={blaetternZu(ergebnis.seite - 1)}
|
||||
className="flex min-h-11 items-center rounded-pille border border-linie px-4 text-sm text-nebentext hover:bg-flaeche-hoch hover:text-text md:min-h-10"
|
||||
>
|
||||
zurück
|
||||
</Link>
|
||||
) : null}
|
||||
<span className="text-sm text-nebentext">
|
||||
Seite {ergebnis.seite} von {ergebnis.seiten}
|
||||
</span>
|
||||
{ergebnis.seite < ergebnis.seiten ? (
|
||||
<Link
|
||||
href={blaetternZu(ergebnis.seite + 1)}
|
||||
className="flex min-h-11 items-center rounded-pille border border-linie px-4 text-sm text-nebentext hover:bg-flaeche-hoch hover:text-text md:min-h-10"
|
||||
>
|
||||
weiter
|
||||
</Link>
|
||||
) : null}
|
||||
</nav>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { ArrowLeft, MessageSquare, Printer, Star } from "lucide-react";
|
||||
import { ArrowLeft, MessageSquare, Printer, ScrollText, Star } from "lucide-react";
|
||||
import { LaufStatus, SchrittTyp, SchrittZustand } from "@/generated/prisma/enums";
|
||||
import { Markdown } from "@/components/markdown";
|
||||
import { AbschnittHuelle } from "@/components/laeufe/abschnitt-huelle";
|
||||
@@ -19,6 +19,7 @@ import { SchrittWert } from "@/components/laeufe/schritt-wert";
|
||||
import { SchrittZustandWaehlen, ZustandKennzeichen } from "@/components/laeufe/schritt-zustand";
|
||||
import { darf } from "@/server/auth/rollen";
|
||||
import { sitzungErforderlich } from "@/server/auth/sitzungspflicht";
|
||||
import { protokollDesLaufs } from "@/server/queries/protokoll";
|
||||
import {
|
||||
kommentareDesLaufs,
|
||||
laufZumAbarbeiten,
|
||||
@@ -293,6 +294,9 @@ export default async function LaufSeite({
|
||||
if (!daten) notFound();
|
||||
|
||||
const kommentare = await kommentareDesLaufs(id, benutzer);
|
||||
// Die Sicht hat `laufHolen` oben schon angewandt; hier geht es nur noch um die
|
||||
// Einträge dieses Laufs (#023).
|
||||
const laufProtokoll = await protokollDesLaufs(id);
|
||||
/*
|
||||
* Die Personenliste nur fuer den Onboarding-Fall holen — sonst stuende in
|
||||
* jedem Seitenaufbau eine Abfrage, die niemand braucht.
|
||||
@@ -416,6 +420,41 @@ export default async function LaufSeite({
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/*
|
||||
* Die Geschichte dieses Laufs, eingeklappt (#023). Sie steht hier und
|
||||
* nicht nur unter Verwaltung → Protokoll, weil die Frage „wer hat das
|
||||
* zurückgeholt“ am Lauf aufkommt und nicht in der Verwaltung. Eingeklappt,
|
||||
* weil ein abgearbeiteter Lauf Dutzende Einträge hat und sie die Schritte
|
||||
* sonst aus dem Bild schieben.
|
||||
*
|
||||
* Kein eigenes Recht: wer den Lauf sehen darf, darf auch sehen, was an ihm
|
||||
* geschehen ist. Die Mandantensicht hat `laufHolen` oben schon angewandt.
|
||||
*/}
|
||||
{laufProtokoll.length > 0 ? (
|
||||
<section className="mt-4 rounded-karte border border-linie bg-flaeche p-4 md:p-5">
|
||||
<details>
|
||||
<summary className="flex min-h-11 cursor-pointer items-center gap-2 text-lg font-semibold md:min-h-0">
|
||||
<ScrollText className="size-5 text-nebentext" strokeWidth={1.75} aria-hidden="true" />
|
||||
Protokoll dieses Laufs
|
||||
<span className="text-sm font-normal text-nebentext">({laufProtokoll.length})</span>
|
||||
</summary>
|
||||
<ol className="mt-3 flex flex-col gap-2">
|
||||
{laufProtokoll.map((eintrag) => (
|
||||
<li key={eintrag.id} className="flex flex-wrap items-baseline gap-x-3 gap-y-1 text-sm">
|
||||
<span className="mono shrink-0 text-xs text-nebentext">
|
||||
{ZEITPUNKT.format(eintrag.erstelltAm)}
|
||||
</span>
|
||||
<span className="min-w-0 break-words">{eintrag.aktion}</span>
|
||||
<span className="text-xs text-nebentext">
|
||||
{eintrag.urheber ? eintrag.urheber.name : "Systemlauf"}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</details>
|
||||
</section>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user