298 lines
9.7 KiB
TypeScript
298 lines
9.7 KiB
TypeScript
"use client";
|
||
|
||
import type { NspdEngineeringNearby } from "@/types/nspd";
|
||
import { useConnectionPoints } from "@/hooks/useConnectionPoints";
|
||
|
||
interface Props {
|
||
nearby: NspdEngineeringNearby[];
|
||
cadNum: string | null | undefined;
|
||
}
|
||
|
||
interface MergedRow {
|
||
label: string;
|
||
type: string | null;
|
||
distanceM: number;
|
||
source: "analyze" | "connection-points";
|
||
// Stable structure identifier (НСПД cad_num) when available — используется для
|
||
// дедупа между источниками, т.к. distanceM меряется до разных опорных точек
|
||
// (analyze — до центроида, connection-points — до границы участка).
|
||
cadNum: string | null;
|
||
// #2111 — расширенные поля из connection-points (EngineeringStructure).
|
||
// analyze-источник их не отдаёт → null; рендерим вторичной строкой только когда есть.
|
||
purpose: string | null;
|
||
characteristics: string | null;
|
||
readableAddress: string | null;
|
||
}
|
||
|
||
export function NspdEngineeringNearbyBlock({ nearby, cadNum }: Props) {
|
||
const { data: cpData, isPending: cpLoading } = useConnectionPoints(cadNum);
|
||
|
||
// Merge and sort by distance
|
||
const rows: MergedRow[] = [];
|
||
|
||
for (const item of nearby) {
|
||
if (item.distance_m !== null) {
|
||
rows.push({
|
||
label: item.name ?? item.type ?? "Объект",
|
||
type: item.type,
|
||
distanceM: item.distance_m,
|
||
source: "analyze",
|
||
cadNum: null,
|
||
purpose: null,
|
||
characteristics: null,
|
||
readableAddress: null,
|
||
});
|
||
}
|
||
}
|
||
|
||
if (cpData) {
|
||
for (const s of cpData.engineering_structures) {
|
||
rows.push({
|
||
label: s.name ?? s.type ?? "Объект",
|
||
type: s.type,
|
||
distanceM: s.distance_to_boundary_m,
|
||
source: "connection-points",
|
||
cadNum: s.cad_num,
|
||
purpose: s.purpose ?? null,
|
||
characteristics: s.characteristics ?? null,
|
||
readableAddress: s.readable_address,
|
||
});
|
||
}
|
||
}
|
||
|
||
// Dedup. distanceM меряется до разных опорных точек (analyze — до центроида,
|
||
// connection-points — до границы), поэтому label+round(distance) пропускает
|
||
// дубли одного физического сооружения. Дедупим по стабильному cad_num, когда он
|
||
// есть; иначе (analyze без cad_num) — по label + округлённому расстоянию.
|
||
const seen = new Set<string>();
|
||
const deduped = rows.filter((r) => {
|
||
const key =
|
||
r.cadNum !== null
|
||
? `cad:${r.cadNum}`
|
||
: `lbl:${r.label}|${Math.round(r.distanceM)}`;
|
||
if (seen.has(key)) return false;
|
||
seen.add(key);
|
||
return true;
|
||
});
|
||
|
||
deduped.sort((a, b) => a.distanceM - b.distanceM);
|
||
|
||
if (deduped.length === 0 && !cpLoading) {
|
||
return (
|
||
<div style={{ fontSize: 13, color: "#6b7280", fontStyle: "italic" }}>
|
||
Инженерных объектов поблизости не найдено
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div>
|
||
{deduped.length > 0 && (
|
||
<table
|
||
style={{
|
||
width: "100%",
|
||
borderCollapse: "collapse",
|
||
fontSize: 13,
|
||
}}
|
||
>
|
||
<thead>
|
||
<tr>
|
||
<th
|
||
style={{
|
||
textAlign: "left",
|
||
padding: "4px 8px",
|
||
color: "#6b7280",
|
||
fontWeight: 600,
|
||
borderBottom: "1px solid #e5e7eb",
|
||
fontSize: 12,
|
||
}}
|
||
>
|
||
Объект
|
||
</th>
|
||
<th
|
||
style={{
|
||
textAlign: "left",
|
||
padding: "4px 8px",
|
||
color: "#6b7280",
|
||
fontWeight: 600,
|
||
borderBottom: "1px solid #e5e7eb",
|
||
fontSize: 12,
|
||
}}
|
||
>
|
||
Тип
|
||
</th>
|
||
<th
|
||
style={{
|
||
textAlign: "right",
|
||
padding: "4px 8px",
|
||
color: "#6b7280",
|
||
fontWeight: 600,
|
||
borderBottom: "1px solid #e5e7eb",
|
||
fontSize: 12,
|
||
}}
|
||
>
|
||
Расстояние
|
||
</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{deduped.map((row, idx) => {
|
||
// #2111 — расширенные поля (НСПД-дамп) одной вторичной строкой под
|
||
// основной; рендерим только присутствующие.
|
||
const hasDetail =
|
||
!!row.purpose ||
|
||
!!row.characteristics ||
|
||
!!row.readableAddress ||
|
||
!!row.cadNum;
|
||
const isLast = idx === deduped.length - 1;
|
||
return (
|
||
<tr
|
||
key={idx}
|
||
style={{
|
||
borderBottom: !isLast ? "1px solid #f3f4f6" : undefined,
|
||
}}
|
||
>
|
||
<td
|
||
colSpan={3}
|
||
style={{
|
||
padding: "5px 8px",
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
alignItems: "baseline",
|
||
gap: 8,
|
||
}}
|
||
>
|
||
<span
|
||
style={{
|
||
flex: 1,
|
||
minWidth: 0,
|
||
color: "#1f2937",
|
||
overflow: "hidden",
|
||
textOverflow: "ellipsis",
|
||
whiteSpace: "nowrap",
|
||
}}
|
||
>
|
||
{row.label}
|
||
</span>
|
||
<span
|
||
style={{
|
||
maxWidth: 140,
|
||
color: "#6b7280",
|
||
overflow: "hidden",
|
||
textOverflow: "ellipsis",
|
||
whiteSpace: "nowrap",
|
||
}}
|
||
>
|
||
{row.type ?? "—"}
|
||
</span>
|
||
<span
|
||
style={{
|
||
color: "#374151",
|
||
textAlign: "right",
|
||
whiteSpace: "nowrap",
|
||
}}
|
||
>
|
||
{Math.round(row.distanceM)} м
|
||
</span>
|
||
</div>
|
||
{hasDetail && (
|
||
<div
|
||
style={{
|
||
marginTop: 3,
|
||
fontSize: 11,
|
||
lineHeight: 1.5,
|
||
color: "#6b7280",
|
||
display: "flex",
|
||
flexDirection: "column",
|
||
gap: 1,
|
||
}}
|
||
>
|
||
{row.purpose && (
|
||
<div>
|
||
<span style={{ color: "#9ca3af" }}>
|
||
Назначение:
|
||
</span>{" "}
|
||
{row.purpose}
|
||
</div>
|
||
)}
|
||
{row.characteristics && (
|
||
<div>
|
||
<span style={{ color: "#9ca3af" }}>
|
||
Характеристики:
|
||
</span>{" "}
|
||
{row.characteristics}
|
||
</div>
|
||
)}
|
||
{row.readableAddress && (
|
||
<div>
|
||
<span style={{ color: "#9ca3af" }}>Адрес:</span>{" "}
|
||
{row.readableAddress}
|
||
</div>
|
||
)}
|
||
{row.cadNum && (
|
||
<div>
|
||
<span style={{ color: "#9ca3af" }}>№ ЕГРН:</span>{" "}
|
||
{row.cadNum}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</td>
|
||
</tr>
|
||
);
|
||
})}
|
||
</tbody>
|
||
</table>
|
||
)}
|
||
|
||
{cpLoading && (
|
||
<div style={{ marginTop: 8, fontSize: 12, color: "#9ca3af" }}>
|
||
Загружаем данные точек подключения…
|
||
</div>
|
||
)}
|
||
|
||
{cpData?.summary && (
|
||
<div
|
||
style={{
|
||
marginTop: 10,
|
||
display: "flex",
|
||
flexWrap: "wrap",
|
||
gap: 8,
|
||
}}
|
||
>
|
||
{cpData.summary.in_protection_zone && (
|
||
<span
|
||
style={{
|
||
background: "#fee2e2",
|
||
color: "#991b1b",
|
||
borderRadius: 6,
|
||
padding: "2px 10px",
|
||
fontSize: 12,
|
||
fontWeight: 600,
|
||
}}
|
||
>
|
||
В охранной зоне
|
||
</span>
|
||
)}
|
||
{cpData.summary.nearest_structure_distance_m !== null && (
|
||
<span
|
||
style={{
|
||
background: "#f3f4f6",
|
||
color: "#374151",
|
||
borderRadius: 6,
|
||
padding: "2px 10px",
|
||
fontSize: 12,
|
||
}}
|
||
>
|
||
Ближайший:{" "}
|
||
{Math.round(cpData.summary.nearest_structure_distance_m)} м
|
||
</span>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|