Der Einstieg am Schreibtisch und das Gedaechtnis der Anwendung. Die Uebersicht
sortiert nach Dringlichkeit — ueberfaellig zuerst, und das ist das Einzige in
Warnfarbe; alles andere auf der Seite ist Auskunft.
Das Archiv sucht ueber Titel, Notizen, erfasste Werte und Kommentare in einer
SQL-Anweisung: `Schrittergebnis.wert` ist ein Json, und nur `("wert")::text`
trifft den Trigramm-Index aus #006. Vor allem aber gehoert die Fundstelle zum
Treffer — ein Treffer ohne sichtbaren Grund ist fuer den Suchenden wertlos.
Die Druckansicht ist ein eigenes Lesemodell und kein Schalter an
`laufZumAbarbeiten`: dort entsteht beim Fuellen der Befehle ein Passwort, und auf
einem Blatt, das jemand abheftet, darf keines stehen. Vom Mitarbeiter holt sie
nur Name, UPN und Abteilung — nicht alles und dann gefiltert.
Der unabhaengige Review fand zwei Blocker, beide im Dashboard: „ueberfaellig“
verglich mit dem Augenblick statt mit dem Tagesbeginn, und der Schnellstart
zaehlte Laeufe fremder Kunden mit. Was „ueberfaellig“ heisst, steht seitdem in
`src/lib/fristen.ts` — eine Regel fuer Lesemodell und Anzeige.
Closes #15
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
128 lines
5.2 KiB
TypeScript
128 lines
5.2 KiB
TypeScript
import { Archive } from "lucide-react";
|
||
import { ArchivFilter, type ArchivFilterfelder } from "@/components/archiv/archiv-filter";
|
||
import { ArchivTreffer } from "@/components/archiv/archiv-treffer";
|
||
import { sitzungErforderlich } from "@/server/auth/sitzungspflicht";
|
||
import { kundenEbeneSichtbar } from "@/server/mandant";
|
||
import {
|
||
ARCHIV_HOECHSTENS,
|
||
SUCHE_MINDESTZEICHEN,
|
||
archivFilterwerte,
|
||
archivSuche,
|
||
datumAusText,
|
||
type ArchivFilter as ArchivFilterwerte,
|
||
} from "@/server/queries/archiv";
|
||
|
||
/**
|
||
* Das Archiv (#015).
|
||
*
|
||
* Das Gedaechtnis der Anwendung: durchsucht werden abgeschlossene und
|
||
* archivierte Laeufe, und zwar in `Lauf.titel`, `Schrittergebnis.notiz`,
|
||
* `Schrittergebnis.wert` und `Kommentar.rumpf`. Was genau dabei geschieht, steht
|
||
* in `src/server/queries/archiv.ts`; diese Seite liest die Adresszeile, prueft
|
||
* ihr Recht und stellt dar.
|
||
*
|
||
* **Sie prueft selbst.** Die Huelle unter `(app)/layout.tsx` ist das zweite Netz
|
||
* und nicht das erste: Next rendert Layouts bei Navigation innerhalb desselben
|
||
* Layouts nicht neu (#007).
|
||
*/
|
||
|
||
export default async function ArchivSeite({
|
||
searchParams,
|
||
}: {
|
||
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
||
}) {
|
||
const { benutzer } = await sitzungErforderlich("lesen", "/archiv");
|
||
const parameter = await searchParams;
|
||
const einzeln = (name: string): string =>
|
||
(Array.isArray(parameter[name]) ? parameter[name][0] : parameter[name])?.trim() ?? "";
|
||
|
||
const felder: ArchivFilterfelder = {
|
||
suche: einzeln("suche"),
|
||
vorlage: einzeln("vorlage"),
|
||
mitarbeiter: einzeln("mitarbeiter"),
|
||
abteilung: einzeln("abteilung"),
|
||
geraet: einzeln("geraet"),
|
||
bearbeiter: einzeln("bearbeiter"),
|
||
kunde: einzeln("kunde"),
|
||
von: einzeln("von"),
|
||
bis: einzeln("bis"),
|
||
laufende: einzeln("laufende") === "1",
|
||
};
|
||
|
||
/*
|
||
* Ein vertippter Parameter filtert nicht, statt eine Fehlerseite zu zeigen –
|
||
* dieselbe Entscheidung wie bei der Geraeteliste (#011). Eine erfundene
|
||
* Kennung bleibt dagegen stehen: sie ist kein Formfehler, sondern eine Suche,
|
||
* die nichts findet. Ueber die Mandantengrenze kommt sie nicht (siehe
|
||
* archivSuche).
|
||
*/
|
||
const von = datumAusText(felder.von);
|
||
const bis = datumAusText(felder.bis);
|
||
const filter: ArchivFilterwerte = {
|
||
...(felder.suche ? { suche: felder.suche } : {}),
|
||
...(felder.vorlage ? { vorlageId: felder.vorlage } : {}),
|
||
...(felder.mitarbeiter ? { mitarbeiterId: felder.mitarbeiter } : {}),
|
||
...(felder.abteilung ? { abteilungId: felder.abteilung } : {}),
|
||
...(felder.geraet ? { geraetId: felder.geraet } : {}),
|
||
...(felder.bearbeiter ? { bearbeiterId: felder.bearbeiter } : {}),
|
||
...(felder.kunde ? { kundeId: felder.kunde } : {}),
|
||
...(von ? { von } : {}),
|
||
...(bis ? { bis } : {}),
|
||
...(felder.laufende ? { auchLaufende: true } : {}),
|
||
};
|
||
|
||
const [ergebnis, werte, kundenEbene] = await Promise.all([
|
||
archivSuche(benutzer, filter),
|
||
archivFilterwerte(benutzer),
|
||
kundenEbeneSichtbar(),
|
||
]);
|
||
// Hervorgehoben wird nur, wonach auch gesucht wurde: ein verworfener Begriff
|
||
// hat keine Fundstellen und darf keine markieren.
|
||
const begriff = ergebnis.sucheZuKurz ? "" : felder.suche;
|
||
|
||
return (
|
||
<>
|
||
<h1 className="flex items-center gap-2 text-2xl font-bold">
|
||
<Archive className="size-6 text-nebentext" strokeWidth={1.75} aria-hidden="true" />
|
||
Archiv
|
||
</h1>
|
||
<p className="mt-2 max-w-prose text-nebentext">
|
||
Durchsucht werden abgeschlossene und archivierte Läufe — ihr Titel, die Notizen und Werte der Schritte
|
||
und die Kommentare. Ein archivierter Lauf ändert sich nicht mehr; zurückholen kann ihn nur ein
|
||
Administrator.
|
||
</p>
|
||
|
||
<ArchivFilter felder={felder} werte={werte} kundenEbene={kundenEbene} />
|
||
|
||
{ergebnis.sucheZuKurz ? (
|
||
<p className="mt-6 rounded-karte border border-linie bg-flaeche p-5 text-sm text-nebentext">
|
||
„{felder.suche}“ ist als Suchbegriff zu kurz — es braucht mindestens {SUCHE_MINDESTZEICHEN} Zeichen.
|
||
Gezeigt werden deshalb die Treffer der übrigen Filter.
|
||
</p>
|
||
) : null}
|
||
|
||
{ergebnis.zeilen.length === 0 ? (
|
||
<p className="mt-6 rounded-karte border border-linie bg-flaeche p-5 text-sm text-nebentext">
|
||
Kein Lauf passt dazu. Laufende Läufe stehen erst mit dem Häkchen „auch offene und laufende Läufe“
|
||
dabei — bis zum Abschluss gehören sie in die Laufliste.
|
||
</p>
|
||
) : (
|
||
<>
|
||
<p className="mt-6 max-w-prose text-sm text-nebentext">
|
||
{ergebnis.zeilen.length === 1 ? "Ein Lauf gefunden" : `${ergebnis.zeilen.length} Läufe gefunden`}
|
||
{/* Eine gekappte Liste sagt das – sonst fehlte ein Lauf stillschweigend. */}
|
||
{ergebnis.gekappt
|
||
? `. Es sind mehr als ${ARCHIV_HOECHSTENS}; gezeigt werden die zuletzt abgelegten. Ein engerer Zeitraum oder ein genauerer Begriff holt die übrigen hervor.`
|
||
: "."}
|
||
</p>
|
||
<ul className="mt-3 flex flex-col gap-3">
|
||
{ergebnis.zeilen.map((zeile) => (
|
||
<ArchivTreffer key={zeile.id} zeile={zeile} begriff={begriff} kundenEbene={kundenEbene} />
|
||
))}
|
||
</ul>
|
||
</>
|
||
)}
|
||
</>
|
||
);
|
||
}
|