From 9a26575a26c83e4a826137e031eccfb542100b6e Mon Sep 17 00:00:00 2001 From: lekss361 Date: Mon, 18 May 2026 07:24:04 +0300 Subject: [PATCH] =?UTF-8?q?feat(sf-legacy):=20expose=20new=20B5=20fields?= =?UTF-8?q?=20(gate=5Fverdict=20+=20utilities=20+=20zouit=20+=20recommenda?= =?UTF-8?q?tion)=20=D0=B2=20=D1=81=D1=82=D0=B0=D1=80=D1=8B=D0=B5=20=D1=82?= =?UTF-8?q?=D0=B0=D0=B1=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлены типы в ParcelAnalysis: utilities, nspd_zouit_overlaps, nspd_engineering_nearby, egrn - Новый компонент NspdZouitOverlapsBlock — зоны с ограничениями ЗОУИТ (warn/danger) - Новый компонент NspdEngineeringNearbyBlock — инженерные сети + охранная зона ЛЭП - Новый компонент EgrnPropertyTable — данные участка из ЕГРН (10 полей) - LandTab: условный рендер трёх новых блоков, расширен hasAny guard - OverviewTab и MarketTab уже содержали GateVerdictBanner/ScoreBreakdownPanel и SuccessRecommendationBlock соответственно --- .../site-finder/EgrnPropertyTable.tsx | 123 ++++++++++++++++++ .../src/components/site-finder/LandTab.tsx | 37 +++++- .../NspdEngineeringNearbyBlock.tsx | 109 ++++++++++++++++ .../site-finder/NspdZouitOverlapsBlock.tsx | 93 +++++++++++++ frontend/src/types/site-finder.ts | 45 +++++++ 5 files changed, 406 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/site-finder/EgrnPropertyTable.tsx create mode 100644 frontend/src/components/site-finder/NspdEngineeringNearbyBlock.tsx create mode 100644 frontend/src/components/site-finder/NspdZouitOverlapsBlock.tsx diff --git a/frontend/src/components/site-finder/EgrnPropertyTable.tsx b/frontend/src/components/site-finder/EgrnPropertyTable.tsx new file mode 100644 index 00000000..1d42c21f --- /dev/null +++ b/frontend/src/components/site-finder/EgrnPropertyTable.tsx @@ -0,0 +1,123 @@ +"use client"; + +import type { EgrnData } from "@/types/site-finder"; + +interface Props { + egrn: EgrnData; +} + +function formatRub(v: number | null): string { + if (v == null) return "—"; + if (v >= 1_000_000) return (v / 1_000_000).toFixed(1) + " млн ₽"; + if (v >= 1_000) return (v / 1_000).toFixed(0) + " тыс ₽"; + return v.toFixed(0) + " ₽"; +} + +function formatArea(v: number | null): string { + if (v == null) return "—"; + return v.toFixed(0) + " м²"; +} + +function formatDate(v: string | null): string { + if (!v) return "—"; + // ISO date → DD.MM.YYYY + const d = new Date(v); + if (isNaN(d.getTime())) return v; + return d.toLocaleDateString("ru-RU"); +} + +const ROWS: Array<{ + label: string; + render: (e: EgrnData) => string; +}> = [ + { label: "Адрес", render: (e) => e.address ?? "—" }, + { label: "Статус участка", render: (e) => e.parcel_status ?? "—" }, + { label: "Категория земель", render: (e) => e.land_category ?? "—" }, + { label: "ВРИ", render: (e) => e.permitted_use_text ?? "—" }, + { label: "Форма собственности", render: (e) => e.ownership_type ?? "—" }, + { label: "Вид права", render: (e) => e.right_type ?? "—" }, + { label: "Площадь по ЕГРН", render: (e) => formatArea(e.area_m2) }, + { + label: "Кадастровая стоимость", + render: (e) => formatRub(e.cadastral_value_rub), + }, + { + label: "Кад. стоимость / м²", + render: (e) => formatRub(e.cadastral_value_per_m2), + }, + { + label: "Дата обновления ЕГРН", + render: (e) => formatDate(e.last_egrn_update_date), + }, +]; + +export function EgrnPropertyTable({ egrn }: Props) { + return ( +
+
+ ЕГРН — данные участка +
+ +
+ {ROWS.map(({ label, render }, idx) => { + const value = render(egrn); + if (value === "—") return null; + return ( +
+ + {label} + + + {value} + +
+ ); + })} +
+
+ ); +} diff --git a/frontend/src/components/site-finder/LandTab.tsx b/frontend/src/components/site-finder/LandTab.tsx index ddc0fed7..da80f6a1 100644 --- a/frontend/src/components/site-finder/LandTab.tsx +++ b/frontend/src/components/site-finder/LandTab.tsx @@ -7,20 +7,55 @@ import { GeologyBlock } from "./GeologyBlock"; import { GeometrySuitabilityBlock } from "./GeometrySuitabilityBlock"; import { GeotechRiskBlock } from "./GeotechRiskBlock"; import { NeighborsBlock } from "./NeighborsBlock"; +import { NspdZouitOverlapsBlock } from "./NspdZouitOverlapsBlock"; +import { NspdEngineeringNearbyBlock } from "./NspdEngineeringNearbyBlock"; +import { EgrnPropertyTable } from "./EgrnPropertyTable"; interface Props { data: ParcelAnalysis; } export function LandTab({ data }: Props) { + const hasZouit = + Array.isArray(data.nspd_zouit_overlaps) && + data.nspd_zouit_overlaps.length > 0; + const hasEngineering = + !!data.utilities?.summary || + (Array.isArray(data.nspd_engineering_nearby) && + data.nspd_engineering_nearby.length > 0); + const hasEgrn = !!data.egrn; const hasAny = data.geotech_risk !== undefined || data.geology !== undefined || data.geometry_suitability !== undefined || - data.neighbors_summary !== undefined; + data.neighbors_summary !== undefined || + hasZouit || + hasEngineering || + hasEgrn; return (
+ {/* ЕГРН — данные участка из реестра */} + {hasEgrn && } + + {/* ЗОУИТ — зоны с особыми условиями использования */} + {Array.isArray(data.nspd_zouit_overlaps) && ( +
+ + Зоны с ограничениями (ЗОУИТ) + + +
+ )} + + {/* Инженерные сети / коммуникации */} + {hasEngineering && ( + + )} + {/* P2 (#46) — Соседи + overlap warning (hard warn если overlap) */} {data.neighbors_summary && ( diff --git a/frontend/src/components/site-finder/NspdEngineeringNearbyBlock.tsx b/frontend/src/components/site-finder/NspdEngineeringNearbyBlock.tsx new file mode 100644 index 00000000..d3ec0d14 --- /dev/null +++ b/frontend/src/components/site-finder/NspdEngineeringNearbyBlock.tsx @@ -0,0 +1,109 @@ +"use client"; + +import type { + NspdEngineeringPoint, + UtilitiesSummary, +} from "@/types/site-finder"; + +interface Props { + utilities?: UtilitiesSummary | null; + points?: NspdEngineeringPoint[]; +} + +export function NspdEngineeringNearbyBlock({ utilities, points }: Props) { + const hasPoints = Array.isArray(points) && points.length > 0; + const hasSummary = !!utilities?.summary; + + if (!hasSummary && !hasPoints) return null; + + return ( +
+
+ Инженерные сети и коммуникации +
+ + {hasSummary && ( +
+ {utilities!.summary} +
+ )} + + {utilities?.power_line_охранная_зона_flag && ( +
+ + Охранная зона ЛЭП — строительство ограничено +
+ )} + + {hasPoints && ( +
+
+ Ближайшие точки подключения +
+ {points!.map((pt, idx) => ( +
+ {pt.name ?? pt.type ?? "Коммуникация"} + {pt.distance_m !== null && ( + + {Math.round(pt.distance_m)} м + + )} +
+ ))} +
+ )} + + {utilities?.note && ( +
{utilities.note}
+ )} +
+ ); +} diff --git a/frontend/src/components/site-finder/NspdZouitOverlapsBlock.tsx b/frontend/src/components/site-finder/NspdZouitOverlapsBlock.tsx new file mode 100644 index 00000000..9b298e7c --- /dev/null +++ b/frontend/src/components/site-finder/NspdZouitOverlapsBlock.tsx @@ -0,0 +1,93 @@ +"use client"; + +import type { NspdZouitOverlap } from "@/types/site-finder"; + +interface Props { + overlaps: NspdZouitOverlap[]; +} + +export function NspdZouitOverlapsBlock({ overlaps }: Props) { + if (overlaps.length === 0) { + return ( +
+ Зоны с ограничениями (ЗОУИТ) не обнаружены +
+ ); + } + + // Deduplicate by name to avoid showing the same zone twice (different sources) + const seen = new Set(); + const unique = overlaps.filter((o) => { + if (seen.has(o.name)) return false; + seen.add(o.name); + return true; + }); + + const isHardBlock = unique.some((o) => + o.type_zone?.toLowerCase().includes("охранная"), + ); + + return ( +
+
+ + + ЗОУИТ: {unique.length}{" "} + {unique.length === 1 ? "зона" : unique.length < 5 ? "зоны" : "зон"} + +
+ +
+ {unique.map((o, idx) => ( +
+
+ {o.type_zone ?? o.layer} +
+
{o.name}
+
+ ))} +
+ +
+ Источник: НСПД / Росреестр ЗОУИТ +
+
+ ); +} diff --git a/frontend/src/types/site-finder.ts b/frontend/src/types/site-finder.ts index 7217fd6b..50129741 100644 --- a/frontend/src/types/site-finder.ts +++ b/frontend/src/types/site-finder.ts @@ -335,6 +335,46 @@ export interface ScoreGroupTotal { contribution_pct: number; } +// B5 extended — utilities (коммуникации) +export interface UtilitiesSummary { + summary: string; + power_line_охранная_зона_flag: boolean; + note: string; +} + +// B5 extended — NSPD ЗОУИТ overlap +export interface NspdZouitOverlap { + group_key: string; + layer: string; + subcategory: string | null; + name: string; + type_zone: string | null; + source: string; +} + +// B5 extended — NSPD engineering nearby (точки подключения) +export interface NspdEngineeringPoint { + name: string | null; + type: string | null; + distance_m: number | null; + lat: number | null; + lon: number | null; +} + +// B5 extended — ЕГРН данные участка +export interface EgrnData { + cadastral_value_rub: number | null; + cadastral_value_per_m2: number | null; + land_category: string | null; + permitted_use_text: string | null; + last_egrn_update_date: string | null; + area_m2: number | null; + ownership_type: string | null; + right_type: string | null; + parcel_status: string | null; + address: string | null; +} + export interface ParcelAnalysis { cad_num: string; source: "cad_quarter" | "cad_building"; @@ -379,6 +419,11 @@ export interface ParcelAnalysis { gate_verdict?: GateVerdict; // D2 (#34) — velocity-score: темп продаж конкурентов velocity?: Velocity | null; + // B5 extended — новые поля анализа + utilities?: UtilitiesSummary | null; + nspd_zouit_overlaps?: NspdZouitOverlap[]; + nspd_engineering_nearby?: NspdEngineeringPoint[]; + egrn?: EgrnData | null; } export type PoiCategory = -- 2.45.3