"use client"; /** * Section1ParcelInfo — "1. Инфо об участке" * * Layout: * HeadlineBar (verdict + score delta subtitle) * 4 KpiCard grid (Площадь / Район / Медиана / Score) * 2-col grid: left = MiniMap, right = EgrnPropertyTable + PoiList2Gis * ExportButtons * * Data: useParcelAnalyzeQuery (B5) + useParcelPoiScoreQuery (B6). * Mock fallback: MOCK_ANALYZE / MOCK_POI_SCORE from mock-toggle.ts. */ import { HeadlineBar } from "@/components/ui/HeadlineBar"; import { KpiCard } from "@/components/site-finder/KpiCard"; import { MiniMap } from "./MiniMap"; import { EgrnPropertyTable } from "./EgrnPropertyTable"; import { NspdVerifyLink } from "./NspdVerifyLink"; import { PoiList2Gis } from "./PoiList2Gis"; import { ExportButtons } from "./ExportButtons"; import { adaptEgrn, formatEncumbrance, useParcelAnalyzeQuery, useParcelPoiScoreQuery, } from "@/lib/site-finder-api"; import type { ParcelEgrn } from "@/lib/site-finder-api"; // ── Fallback EGRN (when B5 extended not yet deployed) ──────────────────────── function buildFallbackEgrn(cad: string): ParcelEgrn { return { cad_num: cad, address: "—", area_m2: NaN, vri: "—", category: "—", registration_date: null, owner_type: "—", encumbrance: "—", status: "—", last_updated: null, }; } // ── Score → verdict label ───────────────────────────────────────────────────── function scoreVerdict(score: number, scoreLabel: string | undefined): string { const label = scoreLabel ?? (score >= 80 ? "отлично" : score >= 65 ? "хорошо" : score >= 45 ? "средне" : "плохо"); const labelUpper = label.toUpperCase(); if (score >= 65) return `ПОДХОДИТ · ${labelUpper}`; if (score >= 45) return `СРЕДНЕ · ${labelUpper}`; return `РИСКИ · ${labelUpper}`; } // ── Section 1 skeleton (loading state) ─────────────────────────────────────── function Section1Skeleton() { return (
{/* HeadlineBar skeleton */}
{/* KPI skeleton */}
{[1, 2, 3, 4].map((i) => (
))}
); } // ── Section 1 error state ───────────────────────────────────────────────────── function Section1Error({ message }: { message: string }) { return (

1. Объект

{message}

); } // ── Main component ──────────────────────────────────────────────────────────── interface Props { cad: string; } export function Section1ParcelInfo({ cad }: Props) { const analyzeQuery = useParcelAnalyzeQuery(cad); const poiQuery = useParcelPoiScoreQuery(cad); if (analyzeQuery.isLoading) { return ; } if (analyzeQuery.isError || !analyzeQuery.data) { return ( ); } const data = analyzeQuery.data; const poiData = poiQuery.data; // EGRN: adapt backend `egrn_block` (snake_case ЕГРН-cad_parcels columns) into // the frontend ParcelEgrn shape. Backend ships `permitted_use_text` / // `land_category` / `ownership_type` / `parcel_status` / `last_egrn_update_date`; // frontend table & CSV expect `vri` / `category` / `owner_type` / `status` / // `last_updated`. Without the adapter 6/10 rows of the ЕГРН table render // empty in prod (#1217). Empty `{}` from backend → null → fallback stub. const egrn: ParcelEgrn = adaptEgrn(data.egrn, cad) ?? buildFallbackEgrn(cad); // #1954 — обременения живут в отдельном `encumbrance_block` (top-level // `encumbrance`), а НЕ в `egrn_block`, поэтому adaptEgrn по дизайну ставит // «—». Подставляем ЗОУИТ-сводку, чтобы строка «Обременения» ЕГРН-таблицы // показывала реальные данные («ЗОУИТ: N (типы)» / «не выявлено (НСПД)») // вместо захардкоженного прочерка. egrn.encumbrance = formatEncumbrance(data.encumbrance); // KPI values const areaHa = egrn.area_m2 > 0 ? `${(egrn.area_m2 / 10000).toFixed(2)} га` : "—"; const district = data.district?.district_name ?? "—"; const medianPrice = data.district?.median_price_per_m2 ? `${(data.district.median_price_per_m2 / 1000).toFixed(0)} тыс. ₽/м²` : "—"; const scoreStr = `${data.score}`; // Score color — пороги выровнены под калиброванную 0–40 шкалу _score_label // (#1871 P2: было 0–100, давало red на хорошем ~30/40 участке). // <5 плохо · 5–15 средне · 15–25 хорошо · ≥25 отлично. const scoreColor = data.score >= 25 ? "green" : data.score >= 15 ? "blue" : data.score >= 5 ? "amber" : "red"; // HeadlineBar texts const headlineTitle = scoreVerdict(data.score, data.score_label); // #1467: при отсутствии score_explanation (старый деплой / частичный мок / // backend без поля) НЕ выдумываем дельту к району — формируем подзаголовок // только из реально присутствующих полей (POI в радиусе 1 км). const headlineSubtitle = data.score_explanation ? data.score_explanation : `${data.poi_count} POI в радиусе 1 км`; return (
{/* Section title + verify-links на официальные источники (#97) */}

1. Объект

{/* HeadlineBar — verdict */}
{/* KPI row — 4 cards */}
{/* 2-column grid: map (left) + EGRN + POI (right) */}
{/* Left: mini-map */} {/* Right: EGRN table + POI list */}
{poiData ? ( ) : ( // Fallback: B6 poi-score (useParcelPoiScoreQuery) ещё грузится или // упал. score_breakdown несёт только name/distance_m — без реального // per-POI weight и без POI-weighted-score, поэтому НЕ подставляем // инвест-балл участка (data.score) как POI-оценку «X / 100» и НЕ // фабрикуем weight/score_contribution (#1466). Показываем честный // пустой стейт «POI данные недоступны» до прихода B6. )}
{/* Export buttons */}
); }