Suche fängt bei „Alle" an, Bereiche in der Kopfzeile sind Knöpfe

**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.
This commit is contained in:
Harlekin
2026-09-03 21:59:06 +02:00
parent d51df03c87
commit eef145c97b
8 changed files with 304 additions and 92 deletions
+2
View File
@@ -133,10 +133,12 @@ export const searchText = {
heading: 'Suche',
placeholder: 'Aktenzeichen, Titel, Inhalt',
kinds: {
all: 'Alle',
case: 'Fallakten',
document: 'Dokumente',
personnel: 'Personalakten',
} as Record<string, string>,
showAllOfKind: (label: string) => `Alle ${label} zeigen`,
filters: 'Filter',
reset: 'Filter zurücksetzen',
empty: 'Kein Treffer. Anderen Begriff oder weniger Filter versuchen.',
+13 -4
View File
@@ -6,12 +6,21 @@
* Darstellung benutzt, ist der Punkt — eine Ansicht ist ein Lesezeichen.
*/
/** Die Arten, über die einzeln gesucht werden kann. */
export const SEARCH_KINDS = ['case', 'document', 'personnel'] as const
export type SearchKind = (typeof SEARCH_KINDS)[number]
/**
* Was in der Adresse stehen kann. `all` ist der Auslieferungszustand: wer
* einen Namen eintippt, will wissen, wo er überhaupt vorkommt, und nicht
* zuerst entscheiden, wonach er sucht. Die einzelnen Arten bleiben als Filter.
*/
export const SEARCH_SCOPES = ['all', ...SEARCH_KINDS] as const
export type SearchScope = (typeof SEARCH_SCOPES)[number]
export type SearchFilters = {
q: string
kind: SearchKind
kind: SearchScope
status?: string
templateKey?: string
classification?: number
@@ -37,9 +46,9 @@ export function parseFilters(params: URLSearchParams | Record<string, string | u
return {
q: (get('q') ?? '').slice(0, 200),
kind: (SEARCH_KINDS as readonly string[]).includes(kind ?? '')
? (kind as SearchKind)
: 'case',
kind: (SEARCH_SCOPES as readonly string[]).includes(kind ?? '')
? (kind as SearchScope)
: 'all',
status: get('status'),
templateKey: get('templateKey'),
classification: stufe !== undefined && /^[0-5]$/.test(stufe) ? Number(stufe) : undefined,
+28 -8
View File
@@ -29,6 +29,13 @@ const FTS_LIMIT = 500
export const PAGE_SIZE = 25
/**
* So viele Treffer je Art zeigt die gemeinsame Ansicht. Weniger als eine volle
* Seite: dort geht es darum, **wo** etwas liegt, nicht darum, alles zu lesen.
* Der Weg in die volle Liste steht daneben.
*/
export const PREVIEW_SIZE = 5
/**
* Personalakten setzen neben der Freigabestufe ein eigenes Recht voraus.
*
@@ -101,7 +108,12 @@ export type CaseHit = {
updatedAt: Date
}
export async function searchCases(p: EffectivePermissions, filters: SearchFilters) {
export async function searchCases(
p: EffectivePermissions,
filters: SearchFilters,
options: { take?: number } = {},
) {
const seite = options.take ?? PAGE_SIZE
const ids = await matchingIds('case', filters.q, p.clearanceLevel)
const where = caseWhere(p, filters, ids)
@@ -118,7 +130,7 @@ export async function searchCases(p: EffectivePermissions, filters: SearchFilter
db.case.findMany({
where: { AND: [where, cursorFilter] },
orderBy: [{ updatedAt: 'desc' }, { id: 'desc' }],
take: PAGE_SIZE + 1,
take: seite + 1,
select: {
id: true,
caseNumber: true,
@@ -136,8 +148,8 @@ export async function searchCases(p: EffectivePermissions, filters: SearchFilter
db.case.groupBy({ by: ['classification'], where, _count: { _all: true } }),
])
const hasMore = treffer.length > PAGE_SIZE
const page = hasMore ? treffer.slice(0, PAGE_SIZE) : treffer
const hasMore = treffer.length > seite
const page = hasMore ? treffer.slice(0, seite) : treffer
const last = page.at(-1)
return {
@@ -171,7 +183,11 @@ function documentWhere(
}
}
export async function searchDocuments(p: EffectivePermissions, filters: SearchFilters) {
export async function searchDocuments(
p: EffectivePermissions,
filters: SearchFilters,
options: { take?: number } = {},
) {
const ids = await matchingIds('document', filters.q, p.clearanceLevel)
const where = documentWhere(p, filters, ids)
@@ -179,7 +195,7 @@ export async function searchDocuments(p: EffectivePermissions, filters: SearchFi
db.document.findMany({
where,
orderBy: { updatedAt: 'desc' },
take: PAGE_SIZE,
take: options.take ?? PAGE_SIZE,
select: {
id: true,
documentNumber: true,
@@ -212,7 +228,11 @@ function personnelWhere(
}
}
export async function searchPersonnel(p: EffectivePermissions, filters: SearchFilters) {
export async function searchPersonnel(
p: EffectivePermissions,
filters: SearchFilters,
options: { take?: number } = {},
) {
// Ohne das Recht gibt es diese Akten für ihn nicht — auch nicht als Zahl.
if (!darfPersonalakten(p)) {
return { hits: [], total: 0, truncated: false }
@@ -225,7 +245,7 @@ export async function searchPersonnel(p: EffectivePermissions, filters: SearchFi
db.personnelFile.findMany({
where,
orderBy: { displayName: 'asc' },
take: PAGE_SIZE,
take: options.take ?? PAGE_SIZE,
select: {
id: true,
displayName: true,