feat(11-06): SavedSearchBar UI — save/load/rename/delete profiles (FILTER-06)

GREEN phase — extends tender-radar-api.ts with listSavedSearches/
createSavedSearch/updateSavedSearch/deleteSavedSearch (plain fetch,
credentials: include), adds SavedSearchBar with the
serializeFiltersFromSearchParams/filtersToSearchParams round-trip helpers
(URL searchParams <-> filters JSON, deliberately excluding page/tender —
navigation state, not filter state), and mounts it above FilterPanel in
page.tsx. Hardcoded German UI per phase convention.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-21 16:50:55 +02:00
parent ca843ab134
commit dd3ff9ea30
3 changed files with 408 additions and 0 deletions
+91
View File
@@ -204,3 +204,94 @@ export async function setTriage(
if (!res.ok) throw new Error('Failed to save tender triage');
return res.json();
}
/**
* A single per-user saved search profile (Plan 11-06, FILTER-06). Per-user,
* NOT tenant-wide (D-11) — the backend derives userId from the auth cookie,
* this client never sends a userId. `filters` mirrors the FilterPanel's URL
* searchParams contract exactly (q, plz, bundesland, region, cpv,
* deadlineFrom/To, openOnly, valueMin/Max, includeNullValue, sort, favOnly)
* — see SavedSearchBar.tsx's serialize/deserialize helpers for the
* round-trip contract.
*/
export interface SavedSearch {
id: string;
name: string;
filters: Record<string, unknown>;
createdAt: string;
updatedAt: string;
}
/** Payload accepted by POST /modules/tender-radar/saved-searches. */
export interface CreateSavedSearchPayload {
name: string;
filters: Record<string, unknown>;
}
/** Payload accepted by PATCH /modules/tender-radar/saved-searches/:searchId. */
export interface UpdateSavedSearchPayload {
name?: string;
filters?: Record<string, unknown>;
}
/**
* List this user's saved search profiles (Plan 11-06, FILTER-06).
* GET /modules/tender-radar/saved-searches
*/
export async function listSavedSearches(): Promise<SavedSearch[]> {
const res = await fetch(`${API_URL}/modules/tender-radar/saved-searches`, {
credentials: 'include',
});
if (!res.ok) throw new Error('Failed to fetch saved searches');
return res.json();
}
/**
* Create a new saved search profile from the currently active filters.
* POST /modules/tender-radar/saved-searches
*/
export async function createSavedSearch(
payload: CreateSavedSearchPayload,
): Promise<SavedSearch> {
const res = await fetch(`${API_URL}/modules/tender-radar/saved-searches`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(payload),
});
if (!res.ok) throw new Error('Failed to create saved search');
return res.json();
}
/**
* Rename and/or update the filters of an existing saved search profile.
* PATCH /modules/tender-radar/saved-searches/:searchId
*/
export async function updateSavedSearch(
id: string,
payload: UpdateSavedSearchPayload,
): Promise<SavedSearch> {
const res = await fetch(
`${API_URL}/modules/tender-radar/saved-searches/${id}`,
{
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(payload),
},
);
if (!res.ok) throw new Error('Failed to update saved search');
return res.json();
}
/**
* Delete a saved search profile.
* DELETE /modules/tender-radar/saved-searches/:searchId
*/
export async function deleteSavedSearch(id: string): Promise<void> {
const res = await fetch(
`${API_URL}/modules/tender-radar/saved-searches/${id}`,
{ method: 'DELETE', credentials: 'include' },
);
if (!res.ok) throw new Error('Failed to delete saved search');
}