From 8a42fba83655bdb8e05889ba8077d0dbdf09086c Mon Sep 17 00:00:00 2001 From: bot-backend Date: Fri, 3 Jul 2026 08:23:59 +0000 Subject: [PATCH] =?UTF-8?q?feat(site-finder):=20=D0=B1=D0=BB=D0=BE=D0=BA?= =?UTF-8?q?=20=C2=AB=D0=A1=D0=B5=D1=82=D0=B8=20=D1=80=D1=8F=D0=B4=D0=BE?= =?UTF-8?q?=D0=BC=20=D1=81=20=D1=83=D1=87=D0=B0=D1=81=D1=82=D0=BA=D0=BE?= =?UTF-8?q?=D0=BC=C2=BB=20=D0=B2=20=C2=A73=20(=D0=A4=D0=B0=D0=B7=D0=B0=20C?= =?UTF-8?q?2-UI)=20(#2282)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../analysis/ResourceReservesCard.tsx | 104 +++++++++++++++++- .../site-finder/connection-capacity.ts | 68 ++++++++++++ frontend/src/hooks/useConnectionCapacity.ts | 1 + 3 files changed, 172 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/site-finder/analysis/ResourceReservesCard.tsx b/frontend/src/components/site-finder/analysis/ResourceReservesCard.tsx index 4f2ba6f1..2df6444e 100644 --- a/frontend/src/components/site-finder/analysis/ResourceReservesCard.tsx +++ b/frontend/src/components/site-finder/analysis/ResourceReservesCard.tsx @@ -15,7 +15,7 @@ * бэка не отработали, показываем нейтральный «загружаются», а не «нет данных». */ -import { Info, Zap, Droplets, Flame, Thermometer } from "lucide-react"; +import { Info, Zap, Droplets, Flame, Thermometer, Route } from "lucide-react"; import type { ConnectionCapacityResponse, @@ -23,6 +23,7 @@ import type { GasGrsRow, HeatCapacityBlock, HeatSystemRow, + NearbyNetworkZone, WaterCapacityRow, } from "@/hooks/useConnectionCapacity"; import { CONNECTION_CAPACITY_RADIUS_M } from "@/hooks/useConnectionCapacity"; @@ -34,9 +35,13 @@ import { formatMva, formatThousandM3Day, formatThousandM3Hour, + formatZoneDistance, humanizeLoadIndex, + humanizeNetKind, + isThermalNear, pickCitySystem, pluralizeSystems, + sortNearbyNetworkZones, toFiniteNumber, } from "@/components/site-finder/connection-capacity"; @@ -494,6 +499,100 @@ function HeatBlock({ heat }: { heat: HeatCapacityBlock | null | undefined }) { ); } +// ── Сети рядом с участком (охранные зоны, #2119 C2) ──────────────────────────── +// +// Где ФИЗИЧЕСКИ проходит сеть (охранный коридор в радиусе 300 м) — позитив-разрез: +// теплотрасса в 24 м → дешёвая врезка. Порядок уже по (distance_m, net_kind) с +// бэка; thermal ≤50 м выводим первым и подсвечиваем success-тоном. + +// Маленький нейтральный чип net_kind (тепло/электро/газ/связь/не уточнено). +function NetKindChip({ netKind }: { netKind: string }) { + return ( + + {humanizeNetKind(netKind)} + + ); +} + +function NearbyNetworkZoneLine({ zone }: { zone: NearbyNetworkZone }) { + const near = isThermalNear(zone); + const distance = formatZoneDistance(zone.distance_m); + + return ( +
+ + + {zone.human_label} + + +
+ ); +} + +function NearbyNetworkZonesBlock({ zones }: { zones: NearbyNetworkZone[] }) { + // Пустой массив → блок не рендерится вовсе (не «нет данных»). + if (zones.length === 0) return null; + + const ordered = sortNearbyNetworkZones(zones); + + return ( +
+
+
+
+ По охранным зонам — где физически проходит сеть. +
+
+ {ordered.map((zone, i) => ( + + ))} +
+
+ ); +} + // ── Card ─────────────────────────────────────────────────────────────────────── const SUB_HEADER: React.CSSProperties = { @@ -573,6 +672,9 @@ export function ResourceReservesCard({ data }: Props) { + {/* Сети рядом (охранные зоны) — рендерится только при наличии зон */} + + {/* Caveat */}
= { + thermal: "тепло", + electricity: "электро", + gas: "газ", + communication: "связь", + general: "не уточнено", +}; + +export function classifyNetKind(raw: string | null | undefined): NetKind { + switch (raw) { + case "thermal": + case "electricity": + case "gas": + case "communication": + return raw; + default: + // Бэк отдаёт "general" для неклассифицированных; неизвестное → тоже general. + return "general"; + } +} + +export function humanizeNetKind(raw: string | null | undefined): string { + return NET_KIND_LABEL[classifyNetKind(raw)]; +} + +// Порог «тепло рядом» — thermal с distance_m ≤ этого выводим первым и подсвечиваем +// позитивным тоном (дешёвая врезка в теплотрассу). Согласован с C2-логикой §3. +export const THERMAL_NEAR_M = 50; + +export function isThermalNear(z: NearbyNetworkZone): boolean { + return ( + classifyNetKind(z.net_kind) === "thermal" && z.distance_m <= THERMAL_NEAR_M + ); +} + +// Дистанция зоны → бейдж. distance_m=0 = зона пересекает участок («по участку»). +export function formatZoneDistance(distanceM: number): string { + if (distanceM <= 0) return "по участку"; + return `в ${Math.round(distanceM).toLocaleString("ru-RU")} м`; +} + +// Сортировка для отображения: сначала thermal-near (главный сигнал), затем — исходный +// порядок бэка (уже (distance_m, net_kind)). Stable — сохраняет тай-брейки сервера. +export function sortNearbyNetworkZones( + zones: readonly NearbyNetworkZone[], +): NearbyNetworkZone[] { + return zones + .map((zone, i) => ({ zone, i })) + .sort((a, b) => { + const aNear = isThermalNear(a.zone) ? 0 : 1; + const bNear = isThermalNear(b.zone) ? 0 : 1; + if (aNear !== bNear) return aNear - bNear; + return a.i - b.i; + }) + .map((x) => x.zone); +} diff --git a/frontend/src/hooks/useConnectionCapacity.ts b/frontend/src/hooks/useConnectionCapacity.ts index e1cf03d4..02efb4dd 100644 --- a/frontend/src/hooks/useConnectionCapacity.ts +++ b/frontend/src/hooks/useConnectionCapacity.ts @@ -18,6 +18,7 @@ export type GasGrsRow = components["schemas"]["GasGrsRow"]; export type GasOutletPoint = components["schemas"]["GasOutletPoint"]; export type HeatCapacityBlock = components["schemas"]["HeatCapacityBlock"]; export type HeatSystemRow = components["schemas"]["HeatSystemRow"]; +export type NearbyNetworkZone = components["schemas"]["NearbyNetworkZone"]; /** * Радиус поиска центров питания (ЦП, ПС 35/110 кВ) вокруг участка. Дефолт 3 км —