190 lines
6.6 KiB
TypeScript
190 lines
6.6 KiB
TypeScript
"use client";
|
||
|
||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||
|
||
import { apiFetch } from "./api";
|
||
import type {
|
||
AggregatedEstimate,
|
||
CianPriceChangeStats,
|
||
HouseAnalyticsResponse,
|
||
HouseInfoForEstimate,
|
||
IMVBenchmarkResponse,
|
||
PlacementHistoryItem,
|
||
SalesVsListingsResponse,
|
||
SellTimeSensitivityResponse,
|
||
StreetDealsResponse,
|
||
TradeInEstimateInput,
|
||
} from "@/types/trade-in";
|
||
|
||
const BASE = "/api/v1/trade-in";
|
||
|
||
/**
|
||
* POST /api/v1/trade-in/estimate
|
||
* Sends apartment parameters and returns AggregatedEstimate (mock data for now).
|
||
*/
|
||
export function useEstimateMutation() {
|
||
return useMutation<AggregatedEstimate, Error, TradeInEstimateInput>({
|
||
mutationFn: (input) =>
|
||
apiFetch<AggregatedEstimate>(`${BASE}/estimate`, {
|
||
method: "POST",
|
||
body: JSON.stringify(input),
|
||
}),
|
||
});
|
||
}
|
||
|
||
/**
|
||
* GET /api/v1/trade-in/estimate/{id}
|
||
* Fetches a previously computed estimate by UUID (for shareable links / PDF).
|
||
*/
|
||
export function useEstimate(estimate_id: string | null) {
|
||
return useQuery<AggregatedEstimate>({
|
||
queryKey: ["trade-in", "estimate", estimate_id],
|
||
queryFn: () =>
|
||
apiFetch<AggregatedEstimate>(`${BASE}/estimate/${estimate_id}`),
|
||
enabled: estimate_id !== null && estimate_id.length > 0,
|
||
staleTime: 10 * 60_000, // 10 min — estimates expire at expires_at anyway
|
||
});
|
||
}
|
||
|
||
/**
|
||
* GET /api/v1/trade-in/estimate/{id}/houses
|
||
* Houses в радиусе 500m от target адреса estimate (Stage 2c data).
|
||
*/
|
||
export function useEstimateHouses(estimate_id: string | null) {
|
||
return useQuery<HouseInfoForEstimate[]>({
|
||
queryKey: ["trade-in", "estimate", estimate_id, "houses"],
|
||
queryFn: () =>
|
||
apiFetch<HouseInfoForEstimate[]>(`${BASE}/estimate/${estimate_id}/houses`),
|
||
enabled: estimate_id !== null && estimate_id.length > 0,
|
||
staleTime: 10 * 60_000,
|
||
});
|
||
}
|
||
|
||
/**
|
||
* GET /api/v1/trade-in/estimate/{id}/placement-history
|
||
* Historical listings from house_placement_history for the target house.
|
||
*/
|
||
export function useEstimatePlacementHistory(estimate_id: string | null) {
|
||
return useQuery<PlacementHistoryItem[]>({
|
||
queryKey: ["trade-in", "estimate", estimate_id, "placement-history"],
|
||
queryFn: () =>
|
||
apiFetch<PlacementHistoryItem[]>(
|
||
`${BASE}/estimate/${estimate_id}/placement-history`,
|
||
),
|
||
enabled: estimate_id !== null && estimate_id.length > 0,
|
||
staleTime: 10 * 60_000,
|
||
});
|
||
}
|
||
|
||
/**
|
||
* GET /api/v1/trade-in/estimate/{id}/imv-benchmark
|
||
* Avito IMV benchmark для UI badge (Stage 3 IMV cache lookup).
|
||
*/
|
||
export function useEstimateImvBenchmark(estimate_id: string | null) {
|
||
return useQuery<IMVBenchmarkResponse>({
|
||
queryKey: ["trade-in", "estimate", estimate_id, "imv-benchmark"],
|
||
queryFn: () =>
|
||
apiFetch<IMVBenchmarkResponse>(`${BASE}/estimate/${estimate_id}/imv-benchmark`),
|
||
enabled: estimate_id !== null && estimate_id.length > 0,
|
||
staleTime: 10 * 60_000,
|
||
});
|
||
}
|
||
|
||
/**
|
||
* GET /api/v1/trade-in/estimate/{id}/cian-price-changes
|
||
* Price change history for Cian analog listings (only those with >0 changes).
|
||
*/
|
||
export function useEstimateCianPriceChanges(estimate_id: string | null) {
|
||
return useQuery<CianPriceChangeStats[]>({
|
||
queryKey: ["trade-in", "estimate", estimate_id, "cian-price-changes"],
|
||
queryFn: () =>
|
||
apiFetch<CianPriceChangeStats[]>(
|
||
`${BASE}/estimate/${estimate_id}/cian-price-changes`,
|
||
),
|
||
enabled: estimate_id !== null && estimate_id.length > 0,
|
||
staleTime: 10 * 60_000,
|
||
});
|
||
}
|
||
|
||
/**
|
||
* GET /api/v1/trade-in/estimate/{id}/house-analytics
|
||
* Price history, KPI and recent sold lots for the target house.
|
||
*/
|
||
export function useEstimateHouseAnalytics(estimate_id: string | null) {
|
||
return useQuery<HouseAnalyticsResponse>({
|
||
queryKey: ["trade-in", "estimate", estimate_id, "house-analytics"],
|
||
queryFn: () =>
|
||
apiFetch<HouseAnalyticsResponse>(
|
||
`${BASE}/estimate/${estimate_id}/house-analytics`,
|
||
),
|
||
enabled: estimate_id !== null && estimate_id.length > 0,
|
||
staleTime: 10 * 60_000,
|
||
});
|
||
}
|
||
|
||
/**
|
||
* GET /api/v1/trade-in/street-deals
|
||
* ДКП-сделки Росреестра по улице target адреса. Per-street (open dataset
|
||
* без номера дома). Empty response если street не извлёкся.
|
||
*/
|
||
export function useStreetDeals(
|
||
address: string | null,
|
||
area_m2: number | null,
|
||
rooms: number | null,
|
||
) {
|
||
const params = new URLSearchParams();
|
||
if (address) params.set("address", address);
|
||
if (area_m2 !== null) params.set("area_m2", String(area_m2));
|
||
if (rooms !== null) params.set("rooms", String(rooms));
|
||
return useQuery<StreetDealsResponse>({
|
||
queryKey: ["trade-in", "street-deals", address, area_m2, rooms],
|
||
queryFn: () =>
|
||
apiFetch<StreetDealsResponse>(`${BASE}/street-deals?${params}`),
|
||
enabled: !!address && area_m2 !== null && rooms !== null,
|
||
staleTime: 5 * 60_000,
|
||
});
|
||
}
|
||
|
||
/**
|
||
* GET /api/v1/trade-in/sales-vs-listings
|
||
* ДКП-сделки + paired listings по улице (PR L / #564 Phase 2). Возвращает все
|
||
* пары LEFT JOIN — сделки без listing match сохраняются (listing_* = null),
|
||
* чтобы вычислить linkage_rate. Используется в StreetDealsCard для показа
|
||
* исторических ASK рядом с фактической ценой сделки.
|
||
*
|
||
* staleTime: 10 минут — данные обновляются раз в сутки (rosreestr import +
|
||
* scrapers), нет смысла часто рефетчить.
|
||
*/
|
||
export function useSalesVsListings(
|
||
address: string | null,
|
||
area_m2: number | null,
|
||
rooms: number | null,
|
||
) {
|
||
const params = new URLSearchParams();
|
||
if (address) params.set("address", address);
|
||
if (area_m2 !== null) params.set("area_m2", String(area_m2));
|
||
if (rooms !== null) params.set("rooms", String(rooms));
|
||
return useQuery<SalesVsListingsResponse>({
|
||
queryKey: ["trade-in", "sales-vs-listings", address, area_m2, rooms],
|
||
queryFn: () =>
|
||
apiFetch<SalesVsListingsResponse>(`${BASE}/sales-vs-listings?${params}`),
|
||
enabled: !!address && area_m2 !== null && rooms !== null,
|
||
staleTime: 10 * 60_000,
|
||
});
|
||
}
|
||
|
||
/**
|
||
* GET /api/v1/trade-in/estimate/{id}/sell-time-sensitivity
|
||
* Median exposure days bucketed by price premium (-5%, 0, +5%, +10%).
|
||
*/
|
||
export function useEstimateSellTimeSensitivity(estimate_id: string | null) {
|
||
return useQuery<SellTimeSensitivityResponse>({
|
||
queryKey: ["trade-in", "estimate", estimate_id, "sell-time-sensitivity"],
|
||
queryFn: () =>
|
||
apiFetch<SellTimeSensitivityResponse>(
|
||
`${BASE}/estimate/${estimate_id}/sell-time-sensitivity`,
|
||
),
|
||
enabled: estimate_id !== null && estimate_id.length > 0,
|
||
staleTime: 10 * 60_000,
|
||
});
|
||
}
|