Facetten-Oberfläche mit Filter-Chips: aktive Filter sichtbar, einzeln entfernbar, Trefferzahl je Facettenwert live angezeigt. - app/api/facets/route.ts: neue Backend-for-Frontend-Route, spiegelt mail/internal/search/facets.go minimal (nur Trefferzahl je Facettenwert). - lib/manticoreQuery.ts: gemeinsamer statischer bool.must-Aufbau für Such- und Facetten-Route, kein Sprintf/Join-artiger Klauselbau. - app/api/search/route.ts (SRC-04): akzeptiert jetzt wiederholbare ?filter=feld:wert-Parameter. - app/FacetPanel.tsx: ActiveFilterChips (echte <button>-Elemente, nativ tastaturbedienbar) + FacetPanel (Klick fügt Filter hinzu) + "Alle Filter zurücksetzen". - lib/filterState.ts: reine Filterzustandsfunktionen, ohne React. Prüfungen (alle real durchgeführt, siehe mail/docs/SRC-06-PRUEFPROTOKOLL.md): 1. Manueller Test gegen echten next start + live Manticore auf 192.168.1.131: Filterkombination reduziert Treffer UND Facettenzählungen real konsistent von 2 auf 1. 2. @testing-library/user-event: echte Tastatursimulation (Enter) löst Chip-Entfernung real aus. 3. 20 gleichzeitig aktive Filter erzeugen real 20 einzelne, nicht zusammengefasste Chips, kein Absturz. Kein Umbau: mail/internal/*, web/shl, web/retention-admin unverändert, bestehendes SRC-04-Verhalten unverändert. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HhgFcLS8tYMhDJpP74C6AQ
65 lines
2.2 KiB
TypeScript
65 lines
2.2 KiB
TypeScript
// SRC-04: dünner Client für die eigene Backend-for-Frontend-Route
|
|
// app/api/search/route.ts. Kein direkter Zugriff auf Manticore oder eine
|
|
// externe Mail-API vom Client aus (Rolle: "Datenzugriff ausschließlich
|
|
// über die bereitgestellte API").
|
|
|
|
import type { FacetField } from "./facetFields";
|
|
|
|
export class ApiError extends Error {}
|
|
|
|
export interface SearchHit {
|
|
messageId: string;
|
|
subjectSnippet: string;
|
|
bodySnippet: string;
|
|
score: number;
|
|
}
|
|
|
|
export interface SearchResponse {
|
|
hits: SearchHit[];
|
|
}
|
|
|
|
export interface AppliedFilter {
|
|
field: FacetField;
|
|
value: string;
|
|
}
|
|
|
|
function filterParams(filters: AppliedFilter[]): [string, string][] {
|
|
return filters.map((f) => ["filter", `${f.field}:${f.value}`]);
|
|
}
|
|
|
|
// tenantSlug: bis zu einer zentralen Session-/IAM-Anbindung (Core-Board-
|
|
// Scope, nicht Bestandteil dieser Kachel) wird der Mandant vom Aufrufer
|
|
// mitgegeben. Die eigentliche Mandantentrennung passiert serverseitig in
|
|
// mail/internal/search (SRC-01/SRC-03), nicht im Frontend.
|
|
export async function search(tenantSlug: string, query: string, filters: AppliedFilter[] = []): Promise<SearchResponse> {
|
|
const params = new URLSearchParams([["tenant", tenantSlug], ["q", query], ...filterParams(filters)]);
|
|
const res = await fetch(`/api/search?${params.toString()}`);
|
|
if (!res.ok) {
|
|
const body = await res.json().catch(() => ({}));
|
|
throw new ApiError(body.error ?? `Suche fehlgeschlagen (${res.status})`);
|
|
}
|
|
return res.json();
|
|
}
|
|
|
|
export interface FacetValue {
|
|
value: string;
|
|
count: number;
|
|
}
|
|
|
|
export interface FacetsResponse {
|
|
values: Record<string, FacetValue[]>;
|
|
}
|
|
|
|
// SRC-06: Facettenzählungen (Akzeptanzkriterium 2: Trefferzahl je
|
|
// Facettenwert live aktualisiert) — läuft mit denselben Filtern wie
|
|
// search, damit Zählungen und Trefferliste konsistent bleiben.
|
|
export async function facets(tenantSlug: string, query: string, filters: AppliedFilter[] = []): Promise<FacetsResponse> {
|
|
const params = new URLSearchParams([["tenant", tenantSlug], ["q", query], ...filterParams(filters)]);
|
|
const res = await fetch(`/api/facets?${params.toString()}`);
|
|
if (!res.ok) {
|
|
const body = await res.json().catch(() => ({}));
|
|
throw new ApiError(body.error ?? `Facetten fehlgeschlagen (${res.status})`);
|
|
}
|
|
return res.json();
|
|
}
|