From 74b52625c8067b04160dc6c80f68ef825baad698 Mon Sep 17 00:00:00 2001 From: Harlekin Date: Thu, 3 Sep 2026 21:59:06 +0200 Subject: [PATCH] =?UTF-8?q?Suche=20f=C3=A4ngt=20bei=20=E2=80=9EAlle"=20an,?= =?UTF-8?q?=20Bereiche=20in=20der=20Kopfzeile=20sind=20Kn=C3=B6pfe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Suche.** Sie startete auf den Fallakten, und man musste die Art vorher wählen — die falsche Reihenfolge: wer einen Namen eintippt, weiss noch nicht, ob er eine Fallakte, ein Dokument oder eine Personalakte sucht. Er will wissen, wo der Name vorkommt. „Alle" ist jetzt der Auslieferungszustand und zeigt je Art fünf Treffer als eigenen Block, mit dem Weg in die volle Liste daneben. Getrennte Blöcke statt einer gemischten Liste nach Rang: ein Aktenzeichen neben einem Namen neben einer Dokumentnummer hat keine erkennbare Ordnung. Die einzelnen Arten bleiben als Filter, und nur dort gibt es Facetten und Blättern — beides bezieht sich auf eine Grundmenge, ein Statusfilter über Fall- und Personalakten zugleich wäre bei der einen ein Filter und bei der anderen ein Ausschluss. **Kopfzeile.** Die Bereiche waren Textlinks: von einem Fliesstextlink nicht zu unterscheiden und ohne Hinweis darauf, wo man gerade ist. Jetzt dieselbe Knopfform wie alles Bedienbare, der aktive Bereich gefüllt. Der gefüllte Zustand trägt hellen Text statt der Akzentfarbe: auf der getönten Fläche wäre Akzent auf Akzent und der Kontrast fiele unter die Untergrenze. Dazu etwas grosszügigere Polsterung an beiden Knopfgrössen, .btn-count für Zahlen in Knöpfen, und kein Doppelring mehr am Profilbild. Nachgesehen mit dem gebauten CSS im Browser, hell und dunkel. Co-Authored-By: Claude Opus 5 --- docs/08-suche-und-export.md | 5 + docs/13-entscheidungen.md | 26 ++++ web/app/(app)/layout.tsx | 23 ++-- web/app/(app)/suche/page.tsx | 236 +++++++++++++++++++++++++---------- web/app/globals.css | 35 +++++- web/components/NavLinks.tsx | 37 ++++++ web/lib/i18n/shell.ts | 2 + web/lib/search/query.ts | 17 ++- web/lib/search/service.ts | 36 ++++-- web/tests/suche.test.ts | 10 +- 10 files changed, 335 insertions(+), 92 deletions(-) create mode 100644 web/components/NavLinks.tsx diff --git a/docs/08-suche-und-export.md b/docs/08-suche-und-export.md index 8cfb4e8..aa042b8 100644 --- a/docs/08-suche-und-export.md +++ b/docs/08-suche-und-export.md @@ -14,6 +14,11 @@ Postgres Full-Text-Search, deutsche Konfiguration. Kein Elasticsearch. Filter: Status, Fallart, Zeitraum, Beteiligte, Klassifizierung, Tags, nur meine Fälle. +**Die Suche sucht ohne Angabe über alle Arten.** Wer einen Namen eintippt, will wissen, wo er +vorkommt, und nicht zuerst entscheiden, wonach er sucht. Die gemeinsame Ansicht zeigt je Art +eine Handvoll Treffer als eigenen Block, mit dem Weg in die volle Liste daneben; die einzelnen +Arten bleiben als Filter, und nur dort gibt es Facetten und Blättern. + **Die Sichtbarkeitsprüfung gehört in dieselbe Abfrage**, nicht als Nachfilter. Sonst stimmen Trefferzahl und Seitenlänge nicht und aus den Lücken lässt sich auf verborgene Akten schließen. Gleiches gilt für Facettenzähler. diff --git a/docs/13-entscheidungen.md b/docs/13-entscheidungen.md index 479be25..3de6a61 100644 --- a/docs/13-entscheidungen.md +++ b/docs/13-entscheidungen.md @@ -1213,3 +1213,29 @@ Der Test prüft die Regel und nicht die Zahl: er liest die Polsterung von `.fiel und verlangt, dass Label-Abstand und Reihenabstand darüber liegen. Wer die Polsterung ändert, fällt dort auf. Nachgemessen im Browser mit dem gebauten CSS: 8 px senkrecht gegen 5,6 px Innenpolsterung, 13,6 px waagerecht gegen 11,2 px. + +### Die Suche fängt bei „Alle" an + +Die Suche startete auf den Fallakten, und man musste die Art vorher wählen. Das ist die +falsche Reihenfolge: wer einen Namen eintippt, weiss noch nicht, ob er eine Fallakte, ein +Dokument oder eine Personalakte sucht — er will wissen, **wo** der Name vorkommt. + +`all` ist deshalb der Auslieferungszustand. Die gemeinsame Ansicht zeigt je Art fünf Treffer +als eigenen Block, mit dem Weg in die volle Liste daneben. Getrennte Blöcke und nicht eine +gemischte Liste nach Rang: ein Aktenzeichen neben einem Namen neben einer Dokumentnummer hat +keine erkennbare Ordnung. + +Facetten und Blättern gibt es weiterhin nur bei einer einzelnen Art. Beides bezieht sich auf +**eine** Grundmenge — ein Statusfilter über Fallakten und Personalakten zugleich wäre bei der +einen Art ein Filter und bei der anderen ein Ausschluss. + +### Die Bereiche in der Kopfzeile sind Knöpfe + +Sie waren Textlinks: von einem Fliesstextlink nicht zu unterscheiden und ohne jeden Hinweis +darauf, wo man gerade ist. Jetzt tragen sie dieselbe Knopfform wie alles Bedienbare, und der +Bereich, in dem man steht, ist gefüllt. + +Der gefüllte Zustand trägt **hellen Text statt der Akzentfarbe**: auf der getönten Fläche wäre +Akzent auf Akzent, und der Kontrast fiele unter die Untergrenze aus docs/10-design-system.md. +Aus demselben Grund liegt in einem Knopf steckende Zahl (`.btn-count`) in Mono und gedämpft — +sie ist eine Angabe, kein Teil der Beschriftung. diff --git a/web/app/(app)/layout.tsx b/web/app/(app)/layout.tsx index 439793e..520c8c7 100644 --- a/web/app/(app)/layout.tsx +++ b/web/app/(app)/layout.tsx @@ -3,6 +3,7 @@ import { signOut } from '@/auth' import { requireUser } from '@/lib/auth/current' import { de } from '@/lib/i18n/de' import { PaletteTrigger } from '@/components/palette/PaletteTrigger' +import { NavLinks } from '@/components/NavLinks' import { ProfileMenu } from '@/components/ProfileMenu' import { Avatar } from '@/components/Avatar' import { SearchBar } from '@/components/SearchBar' @@ -40,15 +41,19 @@ export default async function AppLayout({ children }: { children: React.ReactNod {de.app.shortName} - +
diff --git a/web/app/(app)/suche/page.tsx b/web/app/(app)/suche/page.tsx index 34899e2..33f1da8 100644 --- a/web/app/(app)/suche/page.tsx +++ b/web/app/(app)/suche/page.tsx @@ -2,8 +2,14 @@ import type { Metadata } from 'next' import Link from 'next/link' import { db } from '@/lib/db' import { requireUser } from '@/lib/auth/current' -import { parseFilters, toSearchParams, SEARCH_KINDS } from '@/lib/search/query' import { + parseFilters, + toSearchParams, + SEARCH_KINDS, + SEARCH_SCOPES, +} from '@/lib/search/query' +import { + PREVIEW_SIZE, searchCases, searchCounts, searchDocuments, @@ -49,34 +55,50 @@ export default async function SearchPage({ ]) const levelName = new Map(levels.map((l) => [l.level, l.name])) - const [ergebnis, zahlen] = await Promise.all([ - filters.kind === 'document' - ? searchDocuments(user.permissions, filters) - : filters.kind === 'personnel' - ? searchPersonnel(user.permissions, filters) - : searchCases(user.permissions, filters), - // Die Zahlen der anderen Arten. Ohne sie sucht man nach einem Namen, landet - // auf den Fallakten und erfährt nie, dass es dazu auch eine Personalakte - // gibt — der Reiter sieht leer aus wie jeder andere. + const alle = filters.kind === 'all' + + // In der gemeinsamen Ansicht je Art nur eine Handvoll: dort geht es darum, + // **wo** etwas liegt. Der Weg in die volle Liste steht daneben. + const stueck = alle ? { take: PREVIEW_SIZE } : {} + + const [faelle, dokumente, personen, zahlen] = await Promise.all([ + alle || filters.kind === 'case' ? searchCases(user.permissions, filters, stueck) : null, + alle || filters.kind === 'document' + ? searchDocuments(user.permissions, filters, stueck) + : null, + alle || filters.kind === 'personnel' + ? searchPersonnel(user.permissions, filters, stueck) + : null, + // Die Zahlen aller Arten. Ohne sie sucht man nach einem Namen, landet auf + // den Fallakten und erfährt nie, dass es dazu auch eine Personalakte gibt. searchCounts(user.permissions, filters), ]) - const statusTags = filters.kind === 'personnel' ? await listStatusTags() : [] + // Bei einer einzelnen Art hängt alles Weitere an ihr: Facetten, Blättern, + // die Trefferzahl in der Kopfzeile. Bei „Alle" gibt es das nicht — dort ist + // jede Art ein eigener Block mit eigenem Weg in die Vollansicht. + const ergebnis = alle ? null : (faelle ?? dokumente ?? personen)! + const gesamt = alle + ? zahlen.case + zahlen.document + (zahlen.personnel ?? 0) + : (ergebnis?.total ?? 0) + + const statusTags = + alle || filters.kind === 'personnel' ? await listStatusTags() : [] return (

{de.search.heading}

- {de.search.countLabel(ergebnis.total)} + {de.search.countLabel(gesamt)}