diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e33e6f..12be833 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -127,6 +127,15 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/). keine Kamera läuft, steht ein Feld für den abgetippten Code (#014) - Der Tag-Code in der Geräteliste und in der Tag-Liste führt dorthin, wo auch der Scan landet (#014) +- Übersicht: überfällige Läufe, die eigenen Zuweisungen, zuletzt Bearbeitetes und ein Schnellstart + aus den Vorlagen, aus denen im Haus am häufigsten Läufe entstehen (#015) +- Archiv mit Volltextsuche über Titel, Notizen, erfasste Werte und Kommentare in einem gemeinsamen + Lesemodell; die Trefferliste zeigt, **wo** der Begriff steht, und hebt ihn hervor (#015) +- Filter im Archiv nach Vorlage, Person, Abteilung, Gerät, Zeitraum und Bearbeiter — im Modus + Dienstleistung zusätzlich nach Kunde (#015) +- Druckansicht eines Laufs: hell, ohne Navigation, auf A4, mit allen Schritten, Werten, Notizen, + Namen und Zeitstempeln. Sie ersetzt im MVP den PDF-Export (#015) + ### Sicherheit - Der Compose-Stand bricht ab, wenn `APP_SECRET`, `POSTGRES_PASSWORD` oder `APP_URL` fehlen, @@ -222,3 +231,11 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/). laufen (#014) - Ein Tag, ein Etikett und eine Geräteseite eines fremden Kunden sind nicht erreichbar; „gibt es nicht“ und „darfst du nicht“ sind dieselbe Antwort (#014) +- In der Druckansicht erscheint vom Mitarbeiter nur Name, UPN und Abteilung — das Lesemodell holt + gar nichts anderes erst. Ein Ausdruck liegt herum; was darauf steht, ist aus der Hand gegeben + (#015) +- Für den Ausdruck entsteht **kein** generiertes Passwort: wo in einem Vorlagentext + `{{generiertes_passwort}}` steht, erscheint `[Passwort – nicht gespeichert]`. Zwei Ausdrucke + desselben Laufs sind deshalb Zeichen für Zeichen gleich — der Test prüft genau das (#015) +- Die Archivsuche setzt jeden Wert als Parameter und maskiert `%`, `_` und `\` für `ILIKE`: die + Suche nach „50 %“ trifft nicht „5099“ (#015) diff --git a/docs/architektur.md b/docs/architektur.md index 6467fbb..f188e99 100644 --- a/docs/architektur.md +++ b/docs/architektur.md @@ -240,6 +240,42 @@ ein `'wasm-unsafe-eval'` — die schmale Erlaubnis, WebAssembly zu uebersetzen, an derselben Stelle ein Feld fuer den abgetippten Code: der steht lesbar unter jedem QR, und ein Scanner, der nicht startet, darf keine Sackgasse sein. +## 5f. Archiv und Ausdruck: das Gedaechtnis + +Die **Archivsuche** geht ueber drei Tabellen — `Lauf.titel`, `Schrittergebnis.notiz`, +`Schrittergebnis.wert` und `Kommentar.rumpf` — und steht deshalb als **eine** +SQL-Anweisung in `src/server/queries/archiv.ts` statt als vier Prisma-Abfragen. +Zwei Gruende, beide handfest: + +1. `Schrittergebnis.wert` ist ein Json. Nur `("wert")::text` trifft den + `pg_trgm`-GIN-Index aus #006; eine ganze Json-Spalte als Text zu durchsuchen + kann Prisma nicht. +2. Die Fundstelle gehoert zum Treffer. Wer im Archiv sucht, will nicht wissen + **dass** etwas passt, sondern **wo** — Titel, Notiz, Wert oder Kommentar, mit + dem Ausschnitt drumherum. Vier getrennte Abfragen muessten das hinterher + wieder zusammenlegen. + +Jeder Wert geht als Parameter in die Anweisung (`Prisma.sql`), und `%`, `_` und +`\` werden fuer `ILIKE` maskiert: die Suche nach „50 %“ trifft sonst „5099“. +`tsvector` bleibt die spaetere Optimierung, so wie in §5a festgehalten. + +**Abgelegt heisst `COALESCE(archiviertAm, abgeschlossenAm, erstelltAm)`** — derselbe +Ausdruck fuer die Sortierung **und** den Zeitraumfilter. Zwei verschiedene +Ausdruecke hiessen: ein Lauf steht ausserhalb des Zeitraums, den die Liste an +seiner Position anzeigt. + +Die **Druckansicht** (`/drucken/lauf/`) ist ein eigenes Lesemodell und kein +Schalter an `laufZumAbarbeiten`. Dort entsteht beim Fuellen der Befehle ein +`{{generiertes_passwort}}`; auf einem Blatt, das jemand abheftet, darf keines +stehen — dort erscheint `[Passwort – nicht gespeichert]`, und erzeugt wird +keines. Der Beweis dafuer ist ein Vergleich: zwei Ausdrucke desselben Laufs sind +Zeichen fuer Zeichen gleich, ein erzeugtes Passwort waere bei jedem Aufbau ein +anderes. + +Vom Mitarbeiter holt dieses Lesemodell **nur Name, UPN und Abteilung** — nicht +alles und dann gefiltert. Ein Ausdruck liegt herum, wird kopiert und weitergegeben; +was darauf steht, ist aus der Hand gegeben. + ## 6. Betrieb: zwei Wege, eine Regel Die Datenbank ist auf beiden Wegen von außen nicht erreichbar: diff --git a/docs/devlog.md b/docs/devlog.md index 703ae67..87b73f2 100644 --- a/docs/devlog.md +++ b/docs/devlog.md @@ -17,11 +17,12 @@ als Unterpunkte mit Aufgabe und Dauer. | #012 | M7 – Platzhalter, Namensregeln, Escaping | 2 h 30 min | | #013 | M8 – Läufe: Service und Abarbeitung | 3 h 05 min | | #014 | M9 – QR-Tags, Geräteseite, Etiketten, Scanner | 3 h 35 min | -| | **Summe** | **20 h 25 min** | +| #015 | M10 – Dashboard und Archiv | 2 h 20 min | +| | **Summe** | **22 h 45 min** | | Meilenstein | Tatsächlich | Status | |---|---|---| -| v0.1.0 | 20 h 25 min | in Arbeit | +| v0.1.0 | 22 h 45 min | in Arbeit | --- @@ -625,3 +626,57 @@ als Unterpunkte mit Aufgabe und Dauer. - #015 Dashboard und Archiv — die Übersicht für den Alltag und die Volltextsuche über alles, was abgearbeitet wurde. + +--- + +## 2026-09-25 · Sitzung 11 · 21:35–23:00 + +**Issue:** #015 (M10) +**Version:** 0.1.0 + +### Erledigt + +- **Drei Teile parallel**: Archiv (Sub-Agent), Druckansicht (Sub-Agent), Übersicht (selbst) +- Archiv mit Volltextsuche über Titel, Notizen, Werte und Kommentare, Fundstellen im Treffer, + sechs Filtern und Mandantensicht in jeder Abfrage +- Druckansicht eines Laufs: hell, ohne Navigation, A4 — ersetzt im MVP den PDF-Export +- Übersicht mit Überfälligem, eigenen Zuweisungen, zuletzt Bearbeitetem und Schnellstart +- Unabhängiger Review: **zwei Blocker**, fünf Befunde, neun Anmerkungen — alle abgearbeitet +- **980 Tests** (vorher 930) und **67 Ende-zu-Ende-Tests** grün + +### Entscheidungen + +- **`ABGESCHLOSSEN` gehört ins Archiv.** „Abgeschlossen ist nicht archiviert" betrifft den + Schreibschutz, nicht das Suchen: wer die Seriennummer von letzter Woche sucht, weiß nicht, ob + schon jemand archiviert hat. +- **Eine SQL-Anweisung für drei Tabellen.** `Schrittergebnis.wert` ist ein Json — nur + `("wert")::text` trifft den Trigramm-Index, und über Prisma lässt sich eine Json-Spalte nicht als + Text durchsuchen. Vor allem aber gehört die **Fundstelle** zum Treffer: ein Treffer ohne + sichtbaren Grund ist für den Suchenden wertlos. +- **Die Druckansicht ist ein eigenes Lesemodell**, kein Schalter an `laufZumAbarbeiten`. Dort + entsteht beim Füllen der Befehle ein Passwort; auf einem Blatt, das jemand abheftet, darf keines + stehen. +- **Was „überfällig" heißt, steht jetzt in `src/lib/fristen.ts`** — eine Regel, zwei Benutzer + (Lesemodell und Anzeige). Zwei Fassungen derselben Regel laufen auseinander. + +### Probleme + +- **Beide Blocker steckten in meinem eigenen Teil.** „Überfällig" verglich mit dem Augenblick + statt mit dem Tagesbeginn — ein heute fälliger Lauf stand ab zwei Uhr morgens in Warnfarbe. Und + der Schnellstart zählte Läufe fremder Kunden mit: eine Vorlage ohne Kundenbezug gilt für alle, + ihr Zähler nicht. +- **Mein Test ließ genau die Grenze aus, an der der Fehler saß.** Er kannte „gestern" und + „morgen", aber nicht „heute". Seitdem steht die Regel in einer reinen Funktion mit eigenen + Grenztests. +- **Ein E2E-Test, der nicht scheitern konnte:** „trägt kein Passwort" lief gegen eine Vorlage ohne + Passwortschritt. Er hätte auch bestanden, wenn die Druckansicht Passwörter erzeugte. +- **Der Trigramm-Index auf `Lauf.titel` war strukturell unerreichbar**, weil der Titeltreffer aus + der sechsmal gelesenen und deshalb materialisierten CTE las. Der Review hat das mit 40 000 + Läufen gemessen, nicht vermutet — das ist der Unterschied zwischen „sollte passen" und „passt". +- **Siebter Meilenstein in Folge mit einer Zusage ohne Deckung:** „kein + `{{generiertes_passwort}}` im Druck" galt nur für Vorlagentexte. + +### Nächster Schritt + +- #016 Verzeichnis und LDAP-Abgleich — der größte verbleibende Brocken: `ldapts`, Attribut-Mapping, + Zeitplan, verschlüsseltes Bind-Passwort und ein Integrationstest gegen einen OpenLDAP-Container. diff --git a/docs/issues/015-dashboard-archiv.md b/docs/issues/015-dashboard-archiv.md index 2d34f7a..dd6e491 100644 --- a/docs/issues/015-dashboard-archiv.md +++ b/docs/issues/015-dashboard-archiv.md @@ -1,9 +1,9 @@ # #015 M10 – Dashboard und Archiv -**Status:** offen +**Status:** erledigt **Meilenstein:** v0.1.0 **Labels:** phase-1, frontend, backend -**Geschätzt:** 8 h **Tatsächlich:** – +**Geschätzt:** 8 h **Tatsächlich:** 2 h 20 min ## Ziel @@ -11,18 +11,51 @@ Der Einstieg am Schreibtisch und das Gedächtnis der Anwendung. ## Aufgaben -- [ ] Dashboard: meine offenen Läufe, überfällige Läufe, zuletzt bearbeitet, Schnellstart der häufigsten Vorlagen -- [ ] Archiv-Volltextsuche über Titel, Notizen, Werte und Kommentare in einem gemeinsamen Query-Modul mit `pg_trgm` und `ILIKE` -- [ ] Filter nach Vorlage, Mitarbeiter, Abteilung, Gerät, Zeitraum und Bearbeiter; im Modus `DIENSTLEISTUNG` zusätzlich nach Kunde -- [ ] Druckansicht eines Laufs: Print-CSS, Kopfdaten, alle Abschnitte und Schritte, Werte, Notizen, Namen, Zeitstempel; ersetzt im MVP den PDF-Export -- [ ] In der Druckansicht erscheinen vom Mitarbeiter nur Name, UPN und Abteilung, sonst nichts -- [ ] Screenshots Übersicht, Archiv, Druckansicht in `docs/screenshots/m10/` +- [x] Dashboard: meine offenen Läufe, überfällige Läufe, zuletzt bearbeitet, Schnellstart der häufigsten Vorlagen +- [x] Archiv-Volltextsuche über Titel, Notizen, Werte und Kommentare in einem gemeinsamen Query-Modul mit `pg_trgm` und `ILIKE` +- [x] Filter nach Vorlage, Mitarbeiter, Abteilung, Gerät, Zeitraum und Bearbeiter; im Modus `DIENSTLEISTUNG` zusätzlich nach Kunde +- [x] Druckansicht eines Laufs: Print-CSS, Kopfdaten, alle Abschnitte und Schritte, Werte, Notizen, Namen, Zeitstempel; ersetzt im MVP den PDF-Export +- [x] In der Druckansicht erscheinen vom Mitarbeiter nur Name, UPN und Abteilung, sonst nichts +- [x] Screenshots Übersicht, Archiv, Druckansicht in `docs/screenshots/m10/` ## Akzeptanzkriterien -- [ ] Test: ein archivierter Lauf wird über ein Wort aus einer Schritt-Notiz gefunden -- [ ] Test: ein archivierter Lauf wird über ein Wort aus einem Kommentar gefunden -- [ ] Test: die Filter kombinieren sich (Vorlage und Zeitraum und Bearbeiter) -- [ ] Test: `{{generiertes_passwort}}` erscheint nicht in der Druckansicht -- [ ] Test: überfällige Läufe sind genau die mit `faelligAm < heute` und Status `OFFEN` oder `IN_ARBEIT` -- [ ] Die Druckansicht ist hell, ohne Navigation, und passt auf A4 +- [x] Test: ein archivierter Lauf wird über ein Wort aus einer Schritt-Notiz gefunden +- [x] Test: ein archivierter Lauf wird über ein Wort aus einem Kommentar gefunden +- [x] Test: die Filter kombinieren sich (Vorlage und Zeitraum und Bearbeiter) +- [x] Test: `{{generiertes_passwort}}` erscheint nicht in der Druckansicht +- [x] Test: überfällige Läufe sind genau die mit `faelligAm < heute` und Status `OFFEN` oder `IN_ARBEIT` +- [x] Die Druckansicht ist hell, ohne Navigation, und passt auf A4 + +## Ergebnis + +Erledigt am 2026-09-25. **980 Tests** (vorher 930) und **67 Ende-zu-Ende-Tests** grün, Screenshots in +`docs/screenshots/m10/`. + +Drei Teile entstanden parallel: Archiv, Druckansicht und Übersicht. Eine Entscheidung des +Archiv-Teils ist nicht offensichtlich und darum hier festgehalten: **auch `ABGESCHLOSSEN` gehört +ins Archiv.** „Abgeschlossen ist nicht archiviert" (architektur.md §4) betrifft den Schreibschutz, +nicht das Suchen — wer die Seriennummer von letzter Woche sucht, weiß nicht, ob schon jemand +archiviert hat. + +### Der unabhängige Review + +Zwei Blocker, fünf Befunde, neun Anmerkungen — beide Blocker steckten im Dashboard, also in meinem +eigenen Teil: + +- **„Überfällig" traf auch, was heute fällig ist.** Verglichen wurde mit dem Augenblick statt mit + dem Tagesbeginn; ein Datumsfeld speichert Mitternacht UTC, und damit stand ein heute fälliger + Lauf hierzulande ab zwei Uhr morgens in Warnfarbe — an genau dem Tag, an dem er erledigt werden + soll. Die Regel steht jetzt als `fristVerstrichen()` in `src/lib/fristen.ts`, Lesemodell und + Anzeige benutzen dieselbe, und ein Test prüft die Grenze selbst. Der alte Test kannte nur + „gestern" und „morgen". +- **Der Schnellstart zählte Läufe fremder Kunden mit.** Eine Vorlage ohne Kundenbezug gilt für alle + (#009); ihr Zähler sammelte die Läufe sämtlicher Kunden, und „4× gestartet" war damit eine + Auskunft über fremde Betriebe — dazu eine, die die Reihenfolge nach fremder Arbeit sortierte. + +Dazu die Befunde: der E2E-Test „trägt kein Passwort" lief gegen eine Vorlage **ohne** Passwortschritt +und konnte gar nicht scheitern; der Trigramm-Index auf `Lauf.titel` war strukturell unerreichbar, +weil der Titeltreffer aus der materialisierten CTE las; `pflichtangaben` wurde berechnet, +dokumentiert und nie benutzt; „N weitere in der Liste" führte bei Überfälligen in die ungefilterte +Liste; und die Zusage „kein `{{generiertes_passwort}}` im Druck" galt nur für Vorlagentexte — +wörtlich in eine Notiz geschriebene Zeichenfolgen gibt der Beleg wieder, wie es ein Beleg tun muss. diff --git a/docs/issues/README.md b/docs/issues/README.md index 7abfbcb..767ae1b 100644 --- a/docs/issues/README.md +++ b/docs/issues/README.md @@ -19,9 +19,9 @@ Diese Dateien sind die Quelle; `pnpm issues:sync` spiegelt sie in den Gitea-Trac | #012 | [M7 – Platzhalter, Namensregeln, Escaping](012-platzhalter-namensregeln.md) | erledigt | v0.1.0 | 8 h | 2 h 30 min | | #013 | [M8 – Läufe: Service und Abarbeitung](013-laeufe-abarbeitung.md) | erledigt | v0.1.0 | 14 h | 3 h 05 min | | #014 | [M9 – QR-Tags, Geräteseite, Etiketten, Scanner](014-qr-tags-geraeteseite.md) | erledigt | v0.1.0 | 12 h | 3 h 35 min | -| #015 | [M10 – Dashboard und Archiv](015-dashboard-archiv.md) | offen | v0.1.0 | 8 h | – | +| #015 | [M10 – Dashboard und Archiv](015-dashboard-archiv.md) | erledigt | v0.1.0 | 8 h | 2 h 20 min | | #016 | [M11 – Verzeichnis und LDAP-Abgleich](016-verzeichnis-ldap.md) | offen | v0.1.0 | 12 h | – | | #017 | [M12 – Seed der sieben Vorlagen](017-seed-sieben-vorlagen.md) | offen | v0.1.0 | 8 h | – | | #018 | [M13 – E2E-Tests, Anleitung, PDF](018-e2e-anleitung-pdf.md) | offen | v0.1.0 | 14 h | – | | #019 | [M14 – Betriebswege und Release v0.1.0](019-betrieb-release.md) | offen | v0.1.0 | 10 h | – | -| | **Summe** | | | **136 h** | **20 h 25 min** | +| | **Summe** | | | **136 h** | **22 h 45 min** | diff --git a/docs/screenshots/m10/archiv-desktop.png b/docs/screenshots/m10/archiv-desktop.png new file mode 100644 index 0000000..95dec46 Binary files /dev/null and b/docs/screenshots/m10/archiv-desktop.png differ diff --git a/docs/screenshots/m10/archiv-handy.png b/docs/screenshots/m10/archiv-handy.png new file mode 100644 index 0000000..6b41dec Binary files /dev/null and b/docs/screenshots/m10/archiv-handy.png differ diff --git a/docs/screenshots/m10/druckansicht-desktop.png b/docs/screenshots/m10/druckansicht-desktop.png new file mode 100644 index 0000000..9dc8f8a Binary files /dev/null and b/docs/screenshots/m10/druckansicht-desktop.png differ diff --git a/docs/screenshots/m10/druckansicht-handy.png b/docs/screenshots/m10/druckansicht-handy.png new file mode 100644 index 0000000..b6e8ac2 Binary files /dev/null and b/docs/screenshots/m10/druckansicht-handy.png differ diff --git a/docs/screenshots/m10/uebersicht-desktop.png b/docs/screenshots/m10/uebersicht-desktop.png new file mode 100644 index 0000000..1910be3 Binary files /dev/null and b/docs/screenshots/m10/uebersicht-desktop.png differ diff --git a/docs/screenshots/m10/uebersicht-handy.png b/docs/screenshots/m10/uebersicht-handy.png new file mode 100644 index 0000000..cfd6333 Binary files /dev/null and b/docs/screenshots/m10/uebersicht-handy.png differ diff --git a/scripts/screenshots.ts b/scripts/screenshots.ts index 090b78f..f42a613 100644 --- a/scripts/screenshots.ts +++ b/scripts/screenshots.ts @@ -190,6 +190,26 @@ const SEITEN: readonly Seite[] = [ await tab.waitForURL(/\/t\/LZ-/); }, }, + { name: "archiv", pfad: "/archiv?suche=Inventar" }, + { + /* + * Die Druckansicht braucht einen Lauf, und welcher das ist, weiss nur die + * Datenbank. Also den Weg gehen, den auch ein Mensch geht: von der Laufliste + * aus zum Lauf und von dort auf „Drucken“. + */ + name: "druckansicht", + pfad: "/laeufe", + hell: true, + vorbereiten: async (tab) => { + await tab + .getByRole("link", { name: /NB-001/ }) + .first() + .click(); + await tab.waitForURL(/\/laeufe\/[^/]+$/); + await tab.getByRole("link", { name: "Drucken" }).click(); + await tab.waitForURL(/\/drucken\/lauf\//); + }, + }, { name: "etikett-62x29", pfad: "/drucken/etiketten?layout=etikett", hell: true }, { name: "etiketten-a4", pfad: "/drucken/etiketten?layout=bogen", hell: true }, ]; diff --git a/src/app/(app)/archiv/page.tsx b/src/app/(app)/archiv/page.tsx new file mode 100644 index 0000000..3aff5b5 --- /dev/null +++ b/src/app/(app)/archiv/page.tsx @@ -0,0 +1,127 @@ +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>; +}) { + 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 ( + <> +

+

+

+ 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. +

+ + + + {ergebnis.sucheZuKurz ? ( +

+ „{felder.suche}“ ist als Suchbegriff zu kurz — es braucht mindestens {SUCHE_MINDESTZEICHEN} Zeichen. + Gezeigt werden deshalb die Treffer der übrigen Filter. +

+ ) : null} + + {ergebnis.zeilen.length === 0 ? ( +

+ 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. +

+ ) : ( + <> +

+ {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.` + : "."} +

+
    + {ergebnis.zeilen.map((zeile) => ( + + ))} +
+ + )} + + ); +} diff --git a/src/app/(app)/laeufe/[id]/page.tsx b/src/app/(app)/laeufe/[id]/page.tsx index 8951265..a165049 100644 --- a/src/app/(app)/laeufe/[id]/page.tsx +++ b/src/app/(app)/laeufe/[id]/page.tsx @@ -1,6 +1,6 @@ import Link from "next/link"; import { notFound } from "next/navigation"; -import { ArrowLeft, MessageSquare, Star } from "lucide-react"; +import { ArrowLeft, MessageSquare, Printer, Star } from "lucide-react"; import { LaufStatus, SchrittTyp, SchrittZustand } from "@/generated/prisma/enums"; import { Markdown } from "@/components/markdown"; import { AbschnittHuelle } from "@/components/laeufe/abschnitt-huelle"; @@ -364,6 +364,21 @@ export default async function LaufSeite({ > {ausfuehrlich ? "Kompakt" : "Ausführlich"} + + {/* + * Die Druckansicht liegt ausserhalb der Huelle und traegt deshalb kein + * `Link`-Vorabladen: sie ist hell, ohne Navigation und fuer Papier + * gemacht (#015). Auf dem Blatt steht **kein** Passwort — auch dort + * nicht, wo im Lauf eines stuende. + */} + +