gendesign/frontend/src/hooks/useCustomPois.ts
lekss361 6ecd7a9269 feat(#254,#114): custom POI UI + score breakdown stacked-bar viz
Task A (#254): Leaflet click-to-add custom POIs
- sessionId.ts: getOrCreateSessionId() from localStorage
- types/customPoi.ts: CustomPoi, CustomPoiCreate, CustomPoiUpdate
- hooks/useCustomPois.ts: useCustomPois / useAddCustomPoi / useUpdateCustomPoi / useDeleteCustomPoi (TanStack Query, X-Session-Id header)
- CustomPoiLayer.tsx: LayerGroup with CircleMarker (green/red/gray by weight), popup with edit/delete
- CustomPoiToggleButton.tsx: crosshair mode toggle button
- CustomPoiAddModal.tsx: modal form (name, 26 categories, weight -5..+5, notes)
- CustomPoiEditModal.tsx: same form for editing existing POI
- SiteMap.tsx: MapClickHandler (useMapEvents), integrated all custom POI components
- page.tsx: useCustomPois hook + passes customPois + parcelCad to SiteMap

Task B (#114): ScoreBreakdownStackedBar
- ScoreBreakdownStackedBar.tsx: horizontal stacked bar by category with positive/negative split, toggle between category and factor views, custom POIs highlighted orange with dashed outline
- OverviewTab.tsx: renders ScoreBreakdownStackedBar below ScoreBreakdownPanel

Requires backend PR with /api/v1/custom-pois endpoints merged before POI CRUD works.
2026-05-17 09:35:35 +03:00

128 lines
3.8 KiB
TypeScript

"use client";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiFetch } from "@/lib/api";
import { getOrCreateSessionId } from "@/lib/sessionId";
import type {
CustomPoi,
CustomPoiCreate,
CustomPoiUpdate,
} from "@/types/customPoi";
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function sessionHeaders(): Record<string, string> {
const id = getOrCreateSessionId();
return id ? { "X-Session-Id": id } : {};
}
// ---------------------------------------------------------------------------
// Query key factory
// ---------------------------------------------------------------------------
function customPoisKey(parcelCad?: string | null): unknown[] {
return ["custom-pois", parcelCad ?? null];
}
function analyzeKey(parcelCad: string): unknown[] {
return ["analyze", parcelCad];
}
// ---------------------------------------------------------------------------
// Hooks
// ---------------------------------------------------------------------------
/**
* List custom POIs, optionally filtered by parcel cad number.
*/
export function useCustomPois(parcelCad?: string | null) {
return useQuery({
queryKey: customPoisKey(parcelCad),
queryFn: () => {
const qs = parcelCad
? `?parcel_cad=${encodeURIComponent(parcelCad)}`
: "";
return apiFetch<CustomPoi[]>(`/api/v1/custom-pois${qs}`, {
headers: sessionHeaders(),
});
},
// Don't auto-fetch until session available (SSR guard).
enabled: typeof window !== "undefined",
staleTime: 30_000,
});
}
/**
* Create a custom POI. Invalidates list + analyze query on success.
*/
export function useAddCustomPoi(parcelCad?: string | null) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (payload: CustomPoiCreate) =>
apiFetch<CustomPoi>("/api/v1/custom-pois", {
method: "POST",
body: JSON.stringify(payload),
headers: { ...sessionHeaders() },
}),
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: customPoisKey(parcelCad),
});
// Invalidate global list too
void queryClient.invalidateQueries({ queryKey: customPoisKey(null) });
if (parcelCad) {
void queryClient.invalidateQueries({ queryKey: analyzeKey(parcelCad) });
}
},
});
}
/**
* Update a custom POI by id.
*/
export function useUpdateCustomPoi(parcelCad?: string | null) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: number; data: CustomPoiUpdate }) =>
apiFetch<CustomPoi>(`/api/v1/custom-pois/${id}`, {
method: "PATCH",
body: JSON.stringify(data),
headers: { ...sessionHeaders() },
}),
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: customPoisKey(parcelCad),
});
void queryClient.invalidateQueries({ queryKey: customPoisKey(null) });
if (parcelCad) {
void queryClient.invalidateQueries({ queryKey: analyzeKey(parcelCad) });
}
},
});
}
/**
* Delete a custom POI by id.
*/
export function useDeleteCustomPoi(parcelCad?: string | null) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: number) =>
apiFetch<unknown>(`/api/v1/custom-pois/${id}`, {
method: "DELETE",
headers: sessionHeaders(),
}),
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: customPoisKey(parcelCad),
});
void queryClient.invalidateQueries({ queryKey: customPoisKey(null) });
if (parcelCad) {
void queryClient.invalidateQueries({ queryKey: analyzeKey(parcelCad) });
}
},
});
}