feat(analytics): buildings block + dynamic developer comparison
- 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)
This commit is contained in:
parent
8724a4731b
commit
1b65f55802
4 changed files with 141 additions and 16 deletions
|
|
@ -21,9 +21,20 @@ function DevelopersInner() {
|
|||
(top.data ?? []).map((r) => [r.developer_id, r.developer_name]),
|
||||
);
|
||||
|
||||
// Top-3 by jk_count (complexes count) from the leaderboard, falling back to
|
||||
// static defaults if data is not yet available.
|
||||
const top3Ids = (top.data ?? [])
|
||||
.slice()
|
||||
.sort((a, b) => (b.jk_count ?? 0) - (a.jk_count ?? 0))
|
||||
.slice(0, 3)
|
||||
.map((r) => r.developer_id);
|
||||
|
||||
const defaultTop3 =
|
||||
top3Ids.length >= 3 ? top3Ids : ["6208_0", "5791_0", "5832_0"];
|
||||
|
||||
const compareIds = focusedId
|
||||
? [focusedId, ...["5791_0", "5832_0"].filter((x) => x !== focusedId)]
|
||||
: ["6208_0", "5791_0", "5832_0"];
|
||||
? [focusedId, ...defaultTop3.filter((x) => x !== focusedId).slice(0, 2)]
|
||||
: defaultTop3;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -71,7 +82,7 @@ function DevelopersInner() {
|
|||
title={
|
||||
focusedId && focused
|
||||
? `Сравнение sold% — ${focused.developer_name} vs benchmarks`
|
||||
: "Сравнение sold% — PRINZIP vs Брусника vs Форум"
|
||||
: `Сравнение sold% — топ-3 девелопера`
|
||||
}
|
||||
subtitle="Ежемесячная история DOM.РФ realization (endpoint=developer)."
|
||||
>
|
||||
|
|
@ -79,18 +90,8 @@ function DevelopersInner() {
|
|||
developerIds={compareIds}
|
||||
developerNames={
|
||||
focused
|
||||
? {
|
||||
...topNames,
|
||||
[focused.developer_id]: focused.developer_name,
|
||||
"6208_0": "PRINZIP",
|
||||
"5791_0": "Брусника",
|
||||
"5832_0": "Холдинг Форум-групп",
|
||||
}
|
||||
: {
|
||||
"6208_0": "PRINZIP",
|
||||
"5791_0": "Брусника",
|
||||
"5832_0": "Холдинг Форум-групп",
|
||||
}
|
||||
? { ...topNames, [focused.developer_id]: focused.developer_name }
|
||||
: topNames
|
||||
}
|
||||
/>
|
||||
</Section>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,11 @@ import { useParams } from "next/navigation";
|
|||
import { KpiCard } from "@/components/analytics/KpiCard";
|
||||
import { LazyMount } from "@/components/analytics/LazyMount";
|
||||
import { Section } from "@/components/analytics/Section";
|
||||
import { useObjectDetail, useObjectSalesAgg } from "@/lib/analytics-api";
|
||||
import {
|
||||
useObjectBuildings,
|
||||
useObjectDetail,
|
||||
useObjectSalesAgg,
|
||||
} from "@/lib/analytics-api";
|
||||
|
||||
// Heavy sections deferred so initial paint and main-thread are not blocked.
|
||||
const ObjectSaleChart = dynamic(
|
||||
|
|
@ -67,6 +71,7 @@ export default function ObjectDrillInPage() {
|
|||
|
||||
const detail = useObjectDetail(objId);
|
||||
const agg = useObjectSalesAgg(objId);
|
||||
const buildings = useObjectBuildings(objId);
|
||||
|
||||
const apartments = agg.data?.find((r) => r.type === "apartments");
|
||||
const parking = agg.data?.find((r) => r.type === "parking");
|
||||
|
|
@ -197,6 +202,104 @@ export default function ObjectDrillInPage() {
|
|||
<ObjectPhotoGallery objId={objId} />
|
||||
</LazyMount>
|
||||
</Section>
|
||||
|
||||
{/* Корпуса ЖК — показываем только если есть данные */}
|
||||
{(d?.buildings_count ?? 0) > 0 &&
|
||||
buildings.data &&
|
||||
buildings.data.length > 0 && (
|
||||
<Section
|
||||
title="Корпуса ЖК"
|
||||
subtitle="Данные из NSPD: кадастровый номер, этажность, площадь, назначение."
|
||||
>
|
||||
<div style={{ overflowX: "auto" }}>
|
||||
<table
|
||||
style={{
|
||||
width: "100%",
|
||||
borderCollapse: "collapse",
|
||||
fontSize: 13,
|
||||
}}
|
||||
>
|
||||
<thead>
|
||||
<tr style={{ background: "#f9fafb" }}>
|
||||
{[
|
||||
"Кад. номер",
|
||||
"Этажей",
|
||||
"Площадь, м²",
|
||||
"Назначение",
|
||||
"Название",
|
||||
"Адрес",
|
||||
].map((h) => (
|
||||
<th
|
||||
key={h}
|
||||
style={{
|
||||
padding: "8px 10px",
|
||||
textAlign: "left",
|
||||
fontWeight: 600,
|
||||
borderBottom: "1px solid #e6e8ec",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{h}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{buildings.data.map((b, i) => (
|
||||
<tr
|
||||
key={b.cad_num ?? i}
|
||||
style={{ borderTop: "1px solid #f3f4f6" }}
|
||||
>
|
||||
<td
|
||||
style={{
|
||||
padding: "8px 10px",
|
||||
fontFamily: "monospace",
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
{b.cad_num ?? "—"}
|
||||
</td>
|
||||
<td style={{ padding: "8px 10px" }}>{b.floors ?? "—"}</td>
|
||||
<td style={{ padding: "8px 10px" }}>
|
||||
{b.area != null
|
||||
? b.area.toLocaleString("ru-RU", {
|
||||
maximumFractionDigits: 1,
|
||||
})
|
||||
: "—"}
|
||||
</td>
|
||||
<td style={{ padding: "8px 10px", color: "#5b6066" }}>
|
||||
{b.purpose ?? "—"}
|
||||
</td>
|
||||
<td style={{ padding: "8px 10px" }}>
|
||||
{b.building_name ?? "—"}
|
||||
</td>
|
||||
<td
|
||||
style={{
|
||||
padding: "8px 10px",
|
||||
color: "#5b6066",
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
{b.readable_address ?? "—"}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{/* Placeholder when buildings_count > 0 but data not yet loaded */}
|
||||
{(d?.buildings_count ?? 0) > 0 &&
|
||||
buildings.data &&
|
||||
buildings.data.length === 0 && (
|
||||
<Section title="Корпуса ЖК" subtitle="">
|
||||
<p style={{ color: "#5b6066", fontSize: 13 }}>
|
||||
Геометрия корпусов ещё не подгружена с NSPD.
|
||||
</p>
|
||||
</Section>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import type {
|
|||
DeveloperTopRow,
|
||||
DistrictRow,
|
||||
MarketPulsePoint,
|
||||
ObjectBuilding,
|
||||
ObjectDetail,
|
||||
ObjectInfraPoi,
|
||||
ObjectPhoto,
|
||||
|
|
@ -209,6 +210,15 @@ export function useObjectPhotos(objId: number | string, limit = 100) {
|
|||
});
|
||||
}
|
||||
|
||||
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() {
|
||||
|
|
|
|||
|
|
@ -150,6 +150,17 @@ export interface ObjectDetail {
|
|||
longitude: number | null;
|
||||
obj_status: number | null;
|
||||
snapshot_date: string | null;
|
||||
buildings_count: number | null;
|
||||
}
|
||||
|
||||
export interface ObjectBuilding {
|
||||
cad_num: string | null;
|
||||
floors: number | null;
|
||||
area: number | null;
|
||||
purpose: string | null;
|
||||
building_name: string | null;
|
||||
readable_address: string | null;
|
||||
geom_geojson: unknown;
|
||||
}
|
||||
|
||||
export interface ObjectSaleGraphPoint {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue