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 км —