feat(site-finder): карта точек подключения (НСПД) в секции «Сети» (#1746 part) #1751

Merged
lekss361 merged 1 commit from fix/1746-networks-section-map into main 2026-06-18 08:44:16 +00:00
Showing only changes of commit e89fd66639 - Show all commits

View file

@ -14,6 +14,7 @@
*/
import type { ReactNode } from "react";
import dynamic from "next/dynamic";
import {
Zap,
Flame,
@ -21,11 +22,53 @@ import {
Pipette,
AlertTriangle,
Info,
MapPin,
} from "lucide-react";
import type { Geometry } from "geojson";
import { HeadlineBar } from "@/components/ui/HeadlineBar";
import { NspdVerifyLink } from "./NspdVerifyLink";
import { useParcelAnalyzeQuery } from "@/lib/site-finder-api";
import type { UtilitySummaryItem } from "@/lib/site-finder-api";
import type {
ParcelAnalyzeResponse,
UtilitySummaryItem,
} from "@/lib/site-finder-api";
import type { ParcelAnalysis } from "@/types/site-finder";
import { useConnectionPoints } from "@/hooks/useConnectionPoints";
// ── Connection-points map (#1746) ─────────────────────────────────────────────
// Высота тайла карты для drill-in уровня раздела. Легенда + CpLayerControlPanel
// рендерятся flow-потоком ПОД картой внутри SiteMap, поэтому обёртка НЕ задаёт
// height/overflow (иначе клипнёт контролы — см. урок MiniMap.tsx #1218).
const CP_MAP_HEIGHT = 340;
// Leaflet использует window → монтируем только на клиенте (ssr:false), как в
// MiniMap.tsx. НЕ импортировать SiteMap в server component.
const SiteMap = dynamic(
() =>
import("@/components/site-finder/SiteMap").then((m) => ({
default: m.SiteMap,
})),
{
ssr: false,
loading: () => (
<div
style={{
height: CP_MAP_HEIGHT,
background: "var(--bg-card-alt)",
borderRadius: 12,
border: "1px solid var(--border-card)",
display: "flex",
alignItems: "center",
justifyContent: "center",
color: "var(--fg-tertiary)",
fontSize: 13,
}}
>
Загрузка карты...
</div>
),
},
);
// ── Subtype display config ────────────────────────────────────────────────────
@ -525,6 +568,135 @@ function UtilitiesTable({ items }: UtilitiesTableProps) {
);
}
// ── Connection-points map block (#1746) ───────────────────────────────────────
/**
* Адаптирует /analyze-ответ в минимальный ParcelAnalysis, который SiteMap
* требует обязательным prop `data`. Для контекста карты точек подключения нужен
* только геом участка (geom_geojson) + cad_num + source остальные required-
* поля заполняем безопасными дефолтами (без рыночных слоёв: тут показываем
* только точки подключения + охранные зоны сетей). Зеркалит toSiteMapData из
* MiniMap.tsx, но без проброса competitors/pipeline/opportunity (не нужны).
*/
function toCpMapData(data: ParcelAnalyzeResponse): ParcelAnalysis {
return {
cad_num: data.cad_num,
source: data.source === "cad_building" ? "cad_building" : "cad_quarter",
geom_geojson: (data.geom_geojson as Geometry) ?? null,
score: data.score,
score_breakdown: {},
poi_count: 0,
competitors: [],
noise: null,
air_quality: null,
wind: null,
district: data.district ?? null,
// #255 — ЗОУИТ overlaps (охранные зоны сетей) для слоя ZouitLayer.
nspd_zouit_overlaps: data.nspd_zouit_overlaps ?? undefined,
};
}
interface ConnectionPointsMapProps {
data: ParcelAnalyzeResponse;
}
function ConnectionPointsMap({ data }: ConnectionPointsMapProps) {
// Точки подключения ресурсов (электро/газ/вода/тепло) по кварталу — тянем по
// cad_num и пробрасываем в SiteMap, который рисует ConnectionPointsLayer +
// CpLayerControlPanel при truthy prop `connectionPoints` (паттерн MiniMap.tsx).
const { data: connectionPoints, isLoading } = useConnectionPoints(
data.cad_num,
);
const hasPoints =
!!connectionPoints &&
connectionPoints.dump_available &&
connectionPoints.engineering_structures.length > 0;
return (
<div style={{ marginTop: 16 }}>
{/* Label над картой */}
<div
style={{
display: "flex",
alignItems: "center",
gap: 8,
marginBottom: 4,
}}
>
<MapPin
size={16}
strokeWidth={1.5}
style={{ color: "var(--accent)", flexShrink: 0 }}
/>
<h3
style={{
margin: 0,
fontSize: 14,
fontWeight: 600,
color: "var(--fg-primary)",
}}
>
Точки подключения и охранные зоны сетей (НСПД)
</h3>
</div>
<p
style={{
margin: "0 0 10px",
fontSize: 12,
color: "var(--fg-tertiary)",
}}
>
Инженерные объекты и зоны с особыми условиями из снимка кадастрового
квартала (НСПД). Показывает, где находятся точки подключения, а не только
расстояние до них.
</p>
{isLoading ? (
<div
style={{
height: CP_MAP_HEIGHT,
background: "var(--bg-card-alt)",
borderRadius: 12,
border: "1px solid var(--border-card)",
display: "flex",
alignItems: "center",
justifyContent: "center",
color: "var(--fg-tertiary)",
fontSize: 13,
}}
>
Загрузка точек подключения...
</div>
) : hasPoints ? (
<SiteMap
data={toCpMapData(data)}
connectionPoints={connectionPoints}
mapHeight={CP_MAP_HEIGHT}
/>
) : (
<div
style={{
display: "flex",
alignItems: "center",
gap: 8,
padding: "20px 16px",
background: "var(--bg-card-alt)",
border: "1px dashed var(--border-strong)",
borderRadius: 10,
color: "var(--fg-tertiary)",
fontSize: 13,
}}
>
<Info size={16} strokeWidth={1.5} style={{ flexShrink: 0 }} />
Точки подключения по кварталу не загружены. Снимок инженерной
инфраструктуры НСПД для этого кадастрового квартала отсутствует.
</div>
)}
</div>
);
}
// ── Main component ────────────────────────────────────────────────────────────
interface Props {
@ -640,6 +812,9 @@ export function Section2NetworksUtilities({ cad }: Props) {
{/* Utilities table */}
<UtilitiesTable items={summary} />
{/* Карта точек подключения + охранных зон сетей (#1746) */}
<ConnectionPointsMap data={data} />
{/* Caveat note */}
{note && (
<div