"use client"; /** * ResourceReservesCard — «Ресурсные резервы» для §3 «Сети» (#connection-capacity). * * Показывает свободную мощность для технологического присоединения (ТП) из * ОБЯЗАТЕЛЬНОГО раскрытия сетевых организаций: * • Электричество — ближайший центр питания (ЦП, ПС 35/110 кВ) Россетей со * свободной мощностью + бейдж статуса по индексу загрузки (open/limited/closed). * • Вода/канализация — резервы ЦСВ/ЦСК Водоканала: городская система отдельной * строкой (ОТРИЦАТЕЛЬНЫЙ резерв = дефицит, красный бейдж), посёлковые — * свёрнуты в details. * * Числа приводятся через toFiniteNumber (Pydantic Decimal→str). Пока лоадеры * бэка не отработали, показываем нейтральный «загружаются», а не «нет данных». */ import { Info, Zap, Droplets, Flame } from "lucide-react"; import type { ConnectionCapacityResponse, GasCapacityBlock, GasGrsRow, WaterCapacityRow, } from "@/hooks/useConnectionCapacity"; import { CONNECTION_CAPACITY_RADIUS_M } from "@/hooks/useConnectionCapacity"; import { LOAD_INDEX_BADGE, classifyLoadIndex, formatMeters, formatMva, formatThousandM3Day, formatThousandM3Hour, humanizeLoadIndex, pickCitySystem, pluralizeSystems, toFiniteNumber, } from "@/components/site-finder/connection-capacity"; const RADIUS_KM = CONNECTION_CAPACITY_RADIUS_M / 1000; // Semantic-бейдж (резерв/дефицит) — token-палитра карточек (не карта). const RESERVE_BADGE = { reserve: { label: "резерв", bg: "var(--success-soft)", fg: "var(--success)", }, deficit: { label: "дефицит", bg: "var(--danger-soft)", fg: "var(--danger)", }, depleted: { label: "исчерпана", bg: "var(--danger-soft)", fg: "var(--danger)", }, } as const; function StatusBadge({ label, bg, fg, border, }: { label: string; bg: string; fg: string; border?: string; }) { return ( {label} ); } // ── Электричество ────────────────────────────────────────────────────────────── function PowerRow({ data }: { data: ConnectionCapacityResponse }) { const nearest = data.power_summary.nearest_with_reserve; if (!nearest) { return (
Данные о резервах ПС загружаются.
); } const idx = classifyLoadIndex(nearest.load_index); const badge = idx ? LOAD_INDEX_BADGE[idx] : null; const humanIdx = humanizeLoadIndex(nearest.load_index); const voltage = nearest.voltage_class?.trim() || null; const reserve = formatMva(nearest.reserve_mva); const distance = formatMeters(nearest.distance_m); const parts: string[] = []; if (voltage) parts.push(`${voltage} кВ`); if (reserve) parts.push(`резерв ${reserve}`); if (humanIdx) parts.push(humanIdx); if (distance) parts.push(distance); return (
Ближайший центр питания: {nearest.name} {parts.length > 0 ? ` · ${parts.join(" · ")}` : ""} {badge && ( )}
); } // ── Вода / канализация ───────────────────────────────────────────────────────── function WaterSystemLine({ label, row, }: { label: string; row: WaterCapacityRow; }) { const reserveNum = toFiniteNumber(row.reserve_thousand_m3_day); const reserveStr = formatThousandM3Day(row.reserve_thousand_m3_day); const isDeficit = reserveNum !== null && reserveNum < 0; const badge = isDeficit ? RESERVE_BADGE.deficit : RESERVE_BADGE.reserve; return (
{label}:{" "} {reserveStr ? ( {reserveStr} ) : ( )} {reserveNum !== null && ( )}
{row.note && ( {row.note} )}
); } function WaterBlock({ rows }: { rows: WaterCapacityRow[] }) { if (rows.length === 0) { return (
Данные о резервах водоснабжения загружаются.
); } // Разделяем водоснабжение (ЦСВ, system_kind="water") и водоотведение // (ЦСК, system_kind="sewerage") — значения из бэка (vodokanal_reserve_loader). const supply = rows.filter((r) => r.system_kind === "water"); const sewage = rows.filter((r) => r.system_kind === "sewerage"); const other = rows.filter( (r) => r.system_kind !== "water" && r.system_kind !== "sewerage", ); const citySupply = pickCitySystem(supply); const citySewage = pickCitySystem(sewage); // Остальные (посёлковые) системы — свернуть в details. Городские строки уже // показаны отдельно, поэтому исключаем их из «прочих». const rest = [ ...(citySupply ? supply.filter((r) => r !== citySupply) : supply), ...(citySewage ? sewage.filter((r) => r !== citySewage) : sewage), ...other, ]; // Источник-подпись: период из данных (первый ненулевой). const period = rows.find((r) => r.period)?.period?.trim() || null; return (
{citySupply && ( )} {citySewage && ( )} {rest.length > 0 && (
+ {pluralizeSystems(rest.length)}
{rest.map((r, i) => ( ))}
)} Раскрытие МУП «Водоканал»{period ? ` · ${period}` : ""}
); } // ── Газ ──────────────────────────────────────────────────────────────────────── function GasGrsLine({ row }: { row: GasGrsRow }) { const freeNum = toFiniteNumber(row.free_capacity_th_m3_h); const freeStr = formatThousandM3Hour(row.free_capacity_th_m3_h); const designStr = formatThousandM3Hour(row.design_capacity_th_m3_h); const isDepleted = freeNum !== null && freeNum <= 0; const badge = isDepleted ? RESERVE_BADGE.depleted : RESERVE_BADGE.reserve; const upgrade = row.upgrade_due?.trim() || null; return (
{row.grs_name}: свободно{" "} {freeStr ? ( {freeStr} ) : ( )} {designStr ? ` из ${designStr}` : ""} {row.outputs_count > 1 && ( {" "} ({row.outputs_count} выхода) )} {upgrade && ( {" "} · расшивка: {upgrade} )}
); } function GasBlock({ gas }: { gas: GasCapacityBlock | null | undefined }) { if (!gas || gas.city_grs.length === 0) { return (
Данные о загрузке ГРС загружаются.
); } const totalNum = toFiniteNumber(gas.total_free_th_m3_h); const totalStr = formatThousandM3Hour(gas.total_free_th_m3_h); return (
{gas.city_grs.map((row, i) => ( ))} {totalNum !== null && totalStr && ( Суммарно по городским ГРС: {totalStr} )} Газпром трансгаз Екатеринбург · техвозможность ГРС
); } // ── Card ─────────────────────────────────────────────────────────────────────── const SUB_HEADER: React.CSSProperties = { display: "flex", alignItems: "center", gap: 6, fontSize: 12, fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.04em", color: "var(--fg-tertiary)", marginBottom: 6, }; interface Props { data: ConnectionCapacityResponse; } export function ResourceReservesCard({ data }: Props) { return (

Ресурсные резервы для подключения (ТП)

{/* Электричество */}
{/* Газ */}
{/* Вода / канализация */}
{/* Caveat */}
); }