- ObjectBuilding type + useObjectBuildings hook - Objects detail page: render Корпуса ЖК table (cad_num, floors, area, purpose, name, address) when buildings_count > 0 - Developers page: drop hardcoded compareIds, use top-3 from useTopDevelopers sorted by jk_count DESC (fallback to legacy static array when API has no data yet)
233 lines
6.4 KiB
TypeScript
233 lines
6.4 KiB
TypeScript
"use client";
|
|
|
|
import { keepPreviousData, useMutation, useQuery } from "@tanstack/react-query";
|
|
|
|
import { apiFetch } from "./api";
|
|
import type {
|
|
DeveloperDetail,
|
|
DeveloperHistoryPoint,
|
|
DeveloperPortfolioObject,
|
|
DeveloperTopRow,
|
|
DistrictRow,
|
|
MarketPulsePoint,
|
|
ObjectBuilding,
|
|
ObjectDetail,
|
|
ObjectInfraPoi,
|
|
ObjectPhoto,
|
|
ObjectSaleGraphPoint,
|
|
ObjectSalesAggRow,
|
|
PipelineRow,
|
|
PrinzipDistrictRow,
|
|
PrinzipInsights,
|
|
PrinzipObjectRow,
|
|
QuartirographyDealsRow,
|
|
QuartirographyPortfolioRow,
|
|
RecommendMixInput,
|
|
RecommendMixOutput,
|
|
YandexListingsSummary,
|
|
} from "@/types/analytics";
|
|
|
|
const BASE = "/api/v1/analytics";
|
|
|
|
export function useMarketPulse() {
|
|
return useQuery({
|
|
queryKey: ["analytics", "market-pulse"],
|
|
queryFn: () => apiFetch<MarketPulsePoint[]>(`${BASE}/sverdl/market-pulse`),
|
|
});
|
|
}
|
|
|
|
export function useQuartirographyPortfolio() {
|
|
return useQuery({
|
|
queryKey: ["analytics", "quartirography", "portfolio"],
|
|
queryFn: () =>
|
|
apiFetch<QuartirographyPortfolioRow[]>(
|
|
`${BASE}/sverdl/quartirography?source=portfolio`,
|
|
),
|
|
});
|
|
}
|
|
|
|
export function useQuartirographyDeals() {
|
|
return useQuery({
|
|
queryKey: ["analytics", "quartirography", "deals"],
|
|
queryFn: () =>
|
|
apiFetch<QuartirographyDealsRow[]>(
|
|
`${BASE}/sverdl/quartirography?source=deals`,
|
|
),
|
|
});
|
|
}
|
|
|
|
export function usePipeline() {
|
|
return useQuery({
|
|
queryKey: ["analytics", "pipeline"],
|
|
queryFn: () => apiFetch<PipelineRow[]>(`${BASE}/sverdl/pipeline`),
|
|
});
|
|
}
|
|
|
|
export function useDistricts() {
|
|
return useQuery({
|
|
queryKey: ["analytics", "districts"],
|
|
queryFn: () => apiFetch<DistrictRow[]>(`${BASE}/sverdl/districts`),
|
|
});
|
|
}
|
|
|
|
export function useYandexListings() {
|
|
return useQuery({
|
|
queryKey: ["analytics", "yandex"],
|
|
queryFn: () =>
|
|
apiFetch<YandexListingsSummary>(`${BASE}/sverdl/yandex-listings`),
|
|
});
|
|
}
|
|
|
|
export function useTopDevelopers(limit = 15) {
|
|
return useQuery({
|
|
queryKey: ["analytics", "top-devs", limit],
|
|
queryFn: () =>
|
|
apiFetch<DeveloperTopRow[]>(`${BASE}/developers/top?limit=${limit}`),
|
|
});
|
|
}
|
|
|
|
export function useDeveloperDetail(developerId: string) {
|
|
return useQuery({
|
|
queryKey: ["analytics", "dev", developerId],
|
|
queryFn: () =>
|
|
apiFetch<DeveloperDetail>(`${BASE}/developers/${developerId}`),
|
|
enabled: !!developerId,
|
|
});
|
|
}
|
|
|
|
export function useDeveloperHistory(developerIds: string[]) {
|
|
const idsParam = developerIds.join(",");
|
|
return useQuery({
|
|
queryKey: ["analytics", "dev-history", idsParam],
|
|
queryFn: () =>
|
|
apiFetch<DeveloperHistoryPoint[]>(
|
|
`${BASE}/developers/history?ids=${encodeURIComponent(idsParam)}`,
|
|
),
|
|
enabled: developerIds.length > 0,
|
|
});
|
|
}
|
|
|
|
export function useDeveloperPortfolio(developerId: string) {
|
|
return useQuery({
|
|
queryKey: ["analytics", "dev-portfolio", developerId],
|
|
queryFn: () =>
|
|
apiFetch<DeveloperPortfolioObject[]>(
|
|
`${BASE}/developers/${developerId}/portfolio`,
|
|
),
|
|
enabled: !!developerId,
|
|
});
|
|
}
|
|
|
|
export function usePrinzipInsights() {
|
|
return useQuery({
|
|
queryKey: ["analytics", "prinzip-insights"],
|
|
queryFn: () => apiFetch<PrinzipInsights>(`${BASE}/prinzip/insights`),
|
|
staleTime: Infinity,
|
|
});
|
|
}
|
|
|
|
export function usePrinzipDistricts() {
|
|
return useQuery({
|
|
queryKey: ["analytics", "prinzip-districts"],
|
|
queryFn: () => apiFetch<PrinzipDistrictRow[]>(`${BASE}/prinzip/districts`),
|
|
});
|
|
}
|
|
|
|
export function usePrinzipObjects() {
|
|
return useQuery({
|
|
queryKey: ["analytics", "prinzip-objects"],
|
|
queryFn: () => apiFetch<PrinzipObjectRow[]>(`${BASE}/prinzip/objects`),
|
|
});
|
|
}
|
|
|
|
// Funnel hooks moved to /admin/leads page (CRM data is admin-only).
|
|
|
|
// ── Per-object drill-in ─────────────────────────────────────────────────────
|
|
|
|
export function useObjectDetail(objId: number | string) {
|
|
return useQuery({
|
|
queryKey: ["analytics", "object", objId],
|
|
queryFn: () => apiFetch<ObjectDetail>(`${BASE}/object/${objId}`),
|
|
enabled: !!objId,
|
|
});
|
|
}
|
|
|
|
export function useObjectSaleGraph(objId: number | string, type?: string) {
|
|
return useQuery({
|
|
queryKey: ["analytics", "object-sale-graph", objId, type],
|
|
queryFn: () => {
|
|
const q = type ? `?type=${type}` : "";
|
|
return apiFetch<ObjectSaleGraphPoint[]>(
|
|
`${BASE}/object/${objId}/sale_graph${q}`,
|
|
);
|
|
},
|
|
enabled: !!objId,
|
|
});
|
|
}
|
|
|
|
export function useObjectSalesAgg(objId: number | string) {
|
|
return useQuery({
|
|
queryKey: ["analytics", "object-sales-agg", objId],
|
|
queryFn: () =>
|
|
apiFetch<ObjectSalesAggRow[]>(`${BASE}/object/${objId}/sales_agg`),
|
|
enabled: !!objId,
|
|
});
|
|
}
|
|
|
|
export function useObjectInfrastructure(
|
|
objId: number | string,
|
|
opts?: { category?: string; max_distance?: number },
|
|
) {
|
|
return useQuery({
|
|
queryKey: [
|
|
"analytics",
|
|
"object-infra",
|
|
objId,
|
|
opts?.category,
|
|
opts?.max_distance,
|
|
],
|
|
queryFn: () => {
|
|
const params = new URLSearchParams();
|
|
if (opts?.category) params.set("category", opts.category);
|
|
if (opts?.max_distance != null)
|
|
params.set("max_distance", String(opts.max_distance));
|
|
const qs = params.toString();
|
|
return apiFetch<ObjectInfraPoi[]>(
|
|
`${BASE}/object/${objId}/infrastructure${qs ? "?" + qs : ""}`,
|
|
);
|
|
},
|
|
enabled: !!objId,
|
|
placeholderData: keepPreviousData,
|
|
});
|
|
}
|
|
|
|
export function useObjectPhotos(objId: number | string, limit = 100) {
|
|
return useQuery({
|
|
queryKey: ["analytics", "object-photos", objId, limit],
|
|
queryFn: () =>
|
|
apiFetch<ObjectPhoto[]>(`${BASE}/object/${objId}/photos?limit=${limit}`),
|
|
enabled: !!objId,
|
|
});
|
|
}
|
|
|
|
export function useObjectBuildings(objId: number | string) {
|
|
return useQuery({
|
|
queryKey: ["analytics", "object-buildings", objId],
|
|
queryFn: () =>
|
|
apiFetch<ObjectBuilding[]>(`${BASE}/object/${objId}/buildings`),
|
|
enabled: !!objId,
|
|
});
|
|
}
|
|
|
|
// ── Recommender (Уровень 1) ─────────────────────────────────────────────────
|
|
|
|
export function useRecommendMix() {
|
|
return useMutation({
|
|
mutationFn: (input: RecommendMixInput) =>
|
|
apiFetch<RecommendMixOutput>(`${BASE}/recommend/mix`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(input),
|
|
}),
|
|
});
|
|
}
|