Files
leocrm/frontend/src/api/search.ts
T

114 lines
3.3 KiB
TypeScript
Raw Normal View History

import { apiClient } from './client';
// TODO: P2-F3 — Replace hardcoded ENTITY_URL_MAP with dynamic backend config
export interface SearchResult {
type: string;
id: string;
name: string;
description?: string;
url: string;
score?: number;
data?: Record<string, unknown>;
}
export interface SearchFacet {
value: string;
count: number;
}
export interface SearchFilters {
entityTypes?: string[];
tags?: string[];
dateFrom?: string;
dateTo?: string;
sort?: 'relevance' | 'date' | 'name';
}
export interface SearchResponse {
results: SearchResult[];
facets?: Record<string, SearchFacet[]>;
summary?: string;
suggestions?: string[];
}
export interface FacetsResponse {
entity_types: string[];
tags: string[];
date_ranges: Record<string, { min: string | null; max: string | null }>;
}
// Map backend entity_type to frontend route URL
const ENTITY_URL_MAP: Record<string, (id: string, data?: Record<string, unknown>) => string> = {
contact: (id) => `/contacts/${id}`,
company: (id) => `/contacts/${id}`,
mail: (id) => `/mail?message_id=${id}`,
file: (id) => `/dms?file_id=${id}`,
event: (id) => `/calendar?event_id=${id}`,
message: (_id, data) => data?.conversation_id ? `/communication?conversation_id=${data.conversation_id}` : '/communication',
};
function mapBackendResult(r: Record<string, unknown>): SearchResult {
const entityType = (r.entity_type as string) || 'unknown';
const entityId = (r.entity_id as string) || (r.id as string) || '';
const title = (r.title as string) || (r.name as string) || '';
const snippet = (r.snippet as string) || (r.description as string) || '';
const data = (r.data as Record<string, unknown>) || {};
const urlFn = ENTITY_URL_MAP[entityType];
const url = urlFn ? urlFn(entityId, data) : '#';
return {
type: entityType,
id: entityId,
name: title,
description: snippet,
url,
score: (r.score as number) || 0,
data,
};
}
export async function search(query: string, filters: SearchFilters = {}, limit = 20): Promise<SearchResponse> {
const r = await apiClient.post('/search', {
query,
entity_types: filters.entityTypes,
tags: filters.tags,
date_from: filters.dateFrom,
date_to: filters.dateTo,
sort: filters.sort || 'relevance',
limit,
});
const data = r.data;
// Backend returns { results: [{entity_type, entity_id, title, snippet, score, data}], ... }
const rawResults = data.results || [];
return {
results: rawResults.map(mapBackendResult),
facets: data.facets,
summary: data.summary,
suggestions: data.suggestions,
};
}
export async function fetchFacets(): Promise<FacetsResponse> {
const r = await apiClient.get('/search/facets');
return r.data as FacetsResponse;
}
export async function searchSuggest(q: string): Promise<string[]> {
const r = await apiClient.get('/search/suggest', { params: { q } });
return r.data.suggestions || [];
}
export async function searchSimilar(entityType: string, entityId: string): Promise<SearchResult[]> {
const r = await apiClient.post('/search/similar', {
entity_type: entityType,
entity_id: entityId,
});
const similar = r.data.similar || {};
const all: SearchResult[] = [];
for (const items of Object.values(similar) as Record<string, unknown>[][]) {
for (const item of items) {
all.push(mapBackendResult(item));
}
}
return all;
}