"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 { const id = getOrCreateSessionId(); return id ? { "X-Session-Id": id } : {}; } // --------------------------------------------------------------------------- // Query key factory // --------------------------------------------------------------------------- function customPoisKey(parcelCad?: string | null): unknown[] { // Scope the cache by session: the list is filtered server-side via the // X-Session-Id header (sessionHeaders()), so a session change must yield a // distinct key to avoid serving a previous session's POIs (issue #1484). // getOrCreateSessionId() is SSR-safe (returns "" when window is absent). return ["custom-pois", getOrCreateSessionId(), 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(`/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("/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(`/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(`/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), }); } }, }); }