gendesign/frontend/src/components/site-finder/NspdEngineeringNearbyBlock.tsx
bot-backend 02b5f0b5d1
All checks were successful
Deploy / changes (push) Successful in 8s
Deploy / build-backend (push) Successful in 2m26s
Deploy / build-worker (push) Successful in 3m41s
Deploy / deploy (push) Successful in 1m30s
Deploy / build-frontend (push) Successful in 4m2s
feat(site-finder): max-info попапы — конкурент / §3-таблица / польз.точка / соседи (#2111) (#2113)
2026-06-30 13:45:43 +00:00

298 lines
9.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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>
);
}