diff --git a/app/(app)/employees/page.tsx b/app/(app)/employees/page.tsx index 6cf7ddd..97f8c86 100644 --- a/app/(app)/employees/page.tsx +++ b/app/(app)/employees/page.tsx @@ -6,7 +6,7 @@ import { CARD_CLASS } from "@/components/ui/Card"; import { Pagination } from "@/components/ui/Pagination"; import { StatusChip } from "@/components/ui/StatusChip"; import { currentUserId } from "@/lib/auth/session"; -import { withUser } from "@/lib/db"; +import { sql, withUser } from "@/lib/db"; import { derivedStatusFilter } from "@/lib/employee-status-filter"; import { fmtDate, todayIso } from "@/lib/format"; import { breadcrumbLabel, divisionOf, loadOrgMaps, subtreeOf, unitOf } from "@/lib/org"; @@ -78,15 +78,43 @@ export default async function EmployeesPage({ searchParams }: EmployeesPageProps if (params.q) { const term = params.q.trim(); - if (/^d+$/.test(term)) { + // `\d`, nicht `d`: der fehlende Backslash liess die Ziffernerkennung + // nie greifen — „1590" wurde als Name gesucht und fand nichts, + // während das Muster auf „ddd" ansprang. + if (/^\d+$/.test(term)) { q = q.where("personnel_number", "=", Number(term)); } else { - // Als Parameter gebunden statt in die Abfrage geschrieben: die - // Zeichen, die in der alten Filtersyntax ausbrechen konnten, sind - // hier bedeutungslos. - const like = `%${term}%`; + // Wortweise statt am Stück, und **jedes** Wort muss irgendwo + // treffen. + // + // Vorher wurde der ganze Begriff gegen jedes Feld einzeln geprüft. + // „Michael Winkler" stand damit in keinem Feld — weder Vor- noch + // Nachname enthalten die Zeichenkette — und die Suche kam auf null + // Treffer, obwohl es vierzehn Winkler gibt. Das las sich wie „die + // Person gibt es nicht". + // + // Die Zerlegung macht die Reihenfolge nebensächlich: „Winkler + // micha" und „micha Winkler" führen beide zu Michaela Winkler. Das + // ist mehr, als die Rückmeldung verlangte — sie bat um die + // umgekehrte Reihenfolge —, aber eine Suche, bei der man sich die + // richtige Reihenfolge merken muss, hat dasselbe Problem nur + // gespiegelt. + // + // Verglichen wird gegen den zusammengesetzten Namen, weil genau + // darauf der Trigramm-Index liegt (idx_employees_name_trgm). + // Getrennte Felder hätten ihn ungenutzt gelassen. + const woerter = term.split(/\s+/).filter(Boolean); q = q.where((eb) => - eb.or([eb("first_name", "ilike", like), eb("last_name", "ilike", like), eb("job_title", "ilike", like)]) + eb.and( + woerter.map((wort) => { + // Als Parameter gebunden, nicht in die Abfrage geschrieben. + const like = `%${wort}%`; + return eb.or([ + eb(sql`first_name || ' ' || last_name`, "ilike", like), + eb("job_title", "ilike", like), + ]); + }) + ) ); } }