"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 */}
Резервы — из обязательного раскрытия сетевых организаций в радиусе{" "}
{RADIUS_KM} км; для ТП требуется запрос технических условий (ТУ).
);
}