gendesign/frontend/src/hooks/useCustomPois.ts
Light1YT 01d00ec183
Some checks failed
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Successful in 8s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Has been cancelled
fix(custom-pois): invalidate canonical parcel-analyze + parcel-poi-score keys (#1241)
useCustomPois mutations инвалидировали ['analyze', cad], а такой key
ни один useQuery не регистрирует. Canonical keys: ['parcel-analyze', cad,
horizon] и ['parcel-poi-score', cad]. Все 3 onSuccess были silent no-ops
→ скор+breakdown stale после правок кастомных POI.

Patch: analyzeKey → ['parcel-analyze', parcelCad] (prefix-only, TanStack
covers all horizons), добавлен poiScoreKey, оба инвалидируются вместе.
82/82 frontend тестов зелёные.

Closes #1241
2026-06-13 08:15:05 +00:00

152 lines
4.9 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];
}
// Canonical analyze key prefix is ["parcel-analyze", cad, horizon] (see
// site-finder-api.ts `useParcelAnalyzeQuery`); invalidating with the
// prefix [parcel-analyze, cad] matches every horizon variant. Custom-POI
// add/update/delete also influences the standalone POI score endpoint
// ["parcel-poi-score", cad] (see `useParcelPoiScoreQuery`), so both must
// be invalidated to refresh score + breakdown after a mutation. The
// previous key ["analyze", cad] never matched any live useQuery — bug
// reference: issue #1241.
function analyzeKey(parcelCad: string): unknown[] {
return ["parcel-analyze", parcelCad];
}
function poiScoreKey(parcelCad: string): unknown[] {
return ["parcel-poi-score", 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. On success invalidates:
* - ["custom-pois", parcelCad] (scoped list) + ["custom-pois", null] (global)
* - ["parcel-analyze", parcelCad] prefix (matches every horizon variant)
* - ["parcel-poi-score", parcelCad] (POI weighted score)
*/
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) });
void queryClient.invalidateQueries({
queryKey: poiScoreKey(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) });
void queryClient.invalidateQueries({
queryKey: poiScoreKey(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) });
void queryClient.invalidateQueries({
queryKey: poiScoreKey(parcelCad),
});
}
},
});
}