diff --git a/frontend/src/app/site-finder/analysis/[cad]/page.tsx b/frontend/src/app/site-finder/analysis/[cad]/page.tsx index 4b804421..a8698823 100644 --- a/frontend/src/app/site-finder/analysis/[cad]/page.tsx +++ b/frontend/src/app/site-finder/analysis/[cad]/page.tsx @@ -7,6 +7,7 @@ import { AnalysisSidebar } from "@/components/site-finder/analysis/AnalysisSideb import { AnalysisBreadcrumb } from "@/components/site-finder/analysis/AnalysisBreadcrumb"; import { UserAvatar } from "@/components/site-finder/analysis/UserAvatar"; import { Section1ParcelInfo } from "@/components/site-finder/analysis/Section1ParcelInfo"; +import { Section2NetworksUtilities } from "@/components/site-finder/analysis/Section2NetworksUtilities"; // ── Section placeholder (Wave 3–4 refactor will replace these) ───────────────── @@ -133,12 +134,8 @@ export default function AnalysisPage({ params }: PageProps) { {/* Section 1 — A5: Инфо об участке */} - {/* Section 2 — TODO A6 */} - + {/* Section 2 — A6: Сети и точки подключения */} + {/* Section 3 — TODO A7/A8/A9 */} + ), + }; + case "power_line": + return { + label: "Электричество (ЛЭП)", + icon: ( + + ), + }; + case "pipeline": + return { + label: "Газ (газопровод)", + icon: ( + + ), + }; + case "water_intake": + return { + label: "Водопровод", + icon: ( + + ), + }; + case "pumping_station": + return { + label: "Канализация (насосная)", + icon: ( + + ), + }; + default: + return { + label: subtype, + icon: ( + + ), + }; + } +} + +// ── Distance → presence status ──────────────────────────────────────────────── + +type PresenceStatus = "close" | "reachable" | "far" | "absent"; + +function getPresenceStatus(nearestM: number): PresenceStatus { + if (nearestM <= 200) return "close"; + if (nearestM <= 500) return "reachable"; + if (nearestM <= 1000) return "far"; + return "absent"; +} + +const PRESENCE_LABEL: Record = { + close: "В радиусе 200 м", + reachable: "В радиусе 500 м", + far: "В радиусе 1 км", + absent: "Далее 1 км", +}; + +const PRESENCE_COLOR: Record = { + close: "var(--success)", + reachable: "var(--accent)", + far: "var(--warn)", + absent: "var(--danger)", +}; + +const PRESENCE_BG: Record = { + close: "var(--success-soft)", + reachable: "var(--accent-soft)", + far: "var(--warn-soft)", + absent: "var(--danger-soft)", +}; + +// ── HeadlineBar subtitle helper ─────────────────────────────────────────────── + +function buildSubtitle(items: UtilitySummaryItem[]): string { + const parts: string[] = []; + + const electric = items.find( + (i) => i.subtype === "substation" || i.subtype === "power_line", + ); + const gas = items.find((i) => i.subtype === "pipeline"); + const water = items.find((i) => i.subtype === "water_intake"); + + if (electric) parts.push(`электричество ${electric.nearest_m} м`); + if (gas) parts.push(`газ ${gas.nearest_m} м`); + if (water) parts.push(`водопровод ${water.nearest_m} м`); + + return parts.length > 0 + ? parts.join(" · ") + : "данные из НСПД об инженерной инфраструктуре"; +} + +// ── Loading skeleton ────────────────────────────────────────────────────────── + +function Section2Skeleton() { + return ( +
+
+
+
+ ); +} + +// ── No-data state ───────────────────────────────────────────────────────────── + +function Section2NoData() { + return ( +
+

+ 2. Сети и точки подключения +

+
+ + Данные НСПД об инженерной инфраструктуре не получены для этого участка. + Проверьте наличие NSPD-снимка в базе. +
+
+ ); +} + +// ── Error state ─────────────────────────────────────────────────────────────── + +function Section2Error({ message }: { message: string }) { + return ( +
+

+ 2. Сети и точки подключения +

+

+ {message} +

+
+ ); +} + +// ── Utilities table ─────────────────────────────────────────────────────────── + +interface UtilitiesTableProps { + items: UtilitySummaryItem[]; +} + +function UtilitiesTable({ items }: UtilitiesTableProps) { + return ( +
+ {/* Table header */} +
+
+
+ Тип объекта +
+
+ Ближайший +
+
+ В 2 км +
+
+ Статус +
+
+ + {/* Table rows */} + {items.map((item, idx) => { + const config = getUtilityConfig(item.subtype); + const status = getPresenceStatus(item.nearest_m); + const isLast = idx === items.length - 1; + + return ( +
+ {/* Icon */} +
+ {config.icon} +
+ + {/* Label */} +
+ {config.label} + {item.name && ( + + {item.name} + + )} +
+ + {/* Nearest distance */} +
+ {item.nearest_m.toLocaleString("ru")} м +
+ + {/* Count within 2km */} +
+ {item.count_within_2km} шт. +
+ + {/* Status badge */} +
+ + {PRESENCE_LABEL[status]} + +
+
+ ); + })} +
+ ); +} + +// ── Main component ──────────────────────────────────────────────────────────── + +interface Props { + cad: string; +} + +export function Section2NetworksUtilities({ cad }: Props) { + const { isLoading, isError, error, data } = useParcelAnalyzeQuery(cad); + + if (isLoading) return ; + + if (isError || !data) { + return ( + + ); + } + + const utilities = data.utilities; + + // No utilities data in response (NSPD snapshot absent) + if (!utilities || utilities.summary.length === 0) { + return ; + } + + const { + summary, + power_line_охранная_зона_flag: powerLineZone, + note, + } = utilities; + + // HeadlineBar subtitle: list distances for key utility types + const headlineSubtitle = buildSubtitle(summary); + + return ( +
+ {/* Section title */} +

+ 2. Сети и точки подключения +

+ + {/* HeadlineBar */} +
+ +
+ + {/* Power line охранная зона warning */} + {powerLineZone && ( +
+ + + Участок находится в охранной зоне ЛЭП ≥35 кВ. Капитальное + строительство в ОЗ запрещено (СП 36.13330, ЗОУИТ тип 5). + +
+ )} + + {/* Utilities table */} + + + {/* Caveat note */} + {note && ( +
+ + {note} +
+ )} +
+ ); +} diff --git a/frontend/src/lib/mocks/parcel-analyze.json b/frontend/src/lib/mocks/parcel-analyze.json index 0012bc05..e27c794f 100644 --- a/frontend/src/lib/mocks/parcel-analyze.json +++ b/frontend/src/lib/mocks/parcel-analyze.json @@ -112,5 +112,29 @@ "encumbrance": "Нет", "status": "Учтённый", "last_updated": "2025-10-01" + }, + "utilities": { + "summary": [ + { + "subtype": "substation", + "nearest_m": 59, + "name": null, + "count_within_2km": 7 + }, + { + "subtype": "pipeline", + "nearest_m": 320, + "name": null, + "count_within_2km": 1 + }, + { + "subtype": "power_line", + "nearest_m": 630, + "name": null, + "count_within_2km": 2 + } + ], + "power_line_охранная_зона_flag": false, + "note": "Охранная зона ЛЭП ≥35 кВ — 15-40м по обе стороны (СП 36.13330). В зоне ОЗ нельзя строить капитальные объекты." } } diff --git a/frontend/src/lib/site-finder-api.ts b/frontend/src/lib/site-finder-api.ts index ba62bca8..a7136d8e 100644 --- a/frontend/src/lib/site-finder-api.ts +++ b/frontend/src/lib/site-finder-api.ts @@ -254,6 +254,29 @@ export interface ParcelEgrn { last_updated: string | null; } +// ── Types: utilities (engineering nearby) ──────────────────────────────────── + +export type UtilitySubtype = + | "substation" + | "pipeline" + | "power_line" + | "water_intake" + | "pumping_station" + | string; // open for future subtypes + +export interface UtilitySummaryItem { + subtype: UtilitySubtype; + nearest_m: number; + name: string | null; + count_within_2km: number; +} + +export interface UtilitiesData { + summary: UtilitySummaryItem[]; + power_line_охранная_зона_flag: boolean; + note: string | null; +} + export interface ParcelAnalyzeResponse { cad_num: string; source: string; @@ -279,6 +302,8 @@ export interface ParcelAnalyzeResponse { poi_count: number; /** EGRN data — may be null if B5 extended not yet deployed */ egrn?: ParcelEgrn | null; + /** Engineering / utilities nearby — present when NSPD data available */ + utilities?: UtilitiesData | null; } // ── Types: B6 poi-score response ─────────────────────────────────────────────