gendesign/frontend/src/components/site-finder/NspdRedLinesBlock.tsx
lekss361 fdb54834e5 fix(nspd): rename migration 89→98, fix red lines ST_Area→ST_Length (#220)
- data/sql/98_*: rename from 89 to avoid collision with existing 89_drop_dead_brin
- _get_red_lines: ST_Length(ST_Intersection(planar)::geography) instead of
  ST_Area(ST_Intersection(::geography, ::geography)) — fixes PostGIS 3.4
  tolerance error and returns correct non-zero length for LINESTRING intersections
- Rename intersection_area_sqm → intersection_length_m across schema, TS types,
  frontend component, and tests; add test_get_red_lines_db_exception_returns_empty

Addresses review-bot feedback on PR #220.
2026-05-16 19:33:14 +03:00

136 lines
4.1 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 { RedLine } from "@/types/nspd";
interface Props {
redLines: RedLine[] | null | undefined;
}
function formatLength(m: number | null): string | null {
if (m === null) return null;
if (m >= 1000) return `${(m / 1000).toFixed(2)} км`;
return `${Math.round(m)} м`;
}
function formatDistance(m: number | null): string | null {
if (m === null) return null;
if (m < 1000) return `${Math.round(m)} м`;
return `${(m / 1000).toFixed(1)} км`;
}
export function NspdRedLinesBlock({ redLines }: Props) {
const lines = redLines ?? [];
if (lines.length === 0) {
return null;
}
const intersecting = lines.filter((l) => l.intersection_length_m !== null);
const nearbyOnly = lines.filter((l) => l.intersection_length_m === null);
const hasIntersect = intersecting.length > 0;
return (
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
{/* Alert banner when red lines intersect the parcel */}
{hasIntersect && (
<div
style={{
background: "#fef2f2",
border: "1px solid #fecaca",
borderRadius: 8,
padding: "10px 14px",
display: "flex",
alignItems: "flex-start",
gap: 10,
}}
>
<span
style={{
background: "#fee2e2",
color: "#991b1b",
borderRadius: 5,
padding: "2px 10px",
fontSize: 12,
fontWeight: 700,
whiteSpace: "nowrap",
flexShrink: 0,
}}
>
ПЕРЕСЕЧЕНИЕ
</span>
<div>
<div style={{ fontSize: 13, fontWeight: 600, color: "#991b1b" }}>
Красные линии пересекают участок отступы могут быть критичны
</div>
<div style={{ fontSize: 12, color: "#7f1d1d", marginTop: 4 }}>
{intersecting.map((l, idx) => {
const lenStr = formatLength(l.intersection_length_m);
return (
<div key={idx}>
Линия {idx + 1}
{lenStr ? `: длина пересечения ${lenStr}` : ""}
</div>
);
})}
</div>
</div>
</div>
)}
{/* Nearby-only red lines */}
{nearbyOnly.length > 0 && (
<div
style={{
background: "#fff7ed",
border: "1px solid #fed7aa",
borderRadius: 8,
padding: "10px 14px",
display: "flex",
alignItems: "flex-start",
gap: 10,
}}
>
<span
style={{
background: "#ffedd5",
color: "#9a3412",
borderRadius: 5,
padding: "2px 10px",
fontSize: 12,
fontWeight: 600,
whiteSpace: "nowrap",
flexShrink: 0,
}}
>
РЯДОМ
</span>
<div>
<div style={{ fontSize: 13, fontWeight: 500, color: "#9a3412" }}>
{nearbyOnly.length} кр. лини{nearbyOnly.length === 1 ? "я" : "и"}{" "}
вблизи участка
</div>
<div style={{ fontSize: 12, color: "#7c2d12", marginTop: 4 }}>
{nearbyOnly.map((l, idx) => {
const distStr = formatDistance(l.distance_m);
return (
<div key={idx}>
Линия {idx + 1}
{distStr ? `: ${distStr} от границы` : ""}
</div>
);
})}
</div>
</div>
</div>
)}
{/* Summary line */}
<div style={{ fontSize: 12, color: "#6b7280" }}>
Итого: {lines.length} красн. лини{lines.length === 1 ? "я" : "и"}
{hasIntersect
? `, из них ${intersecting.length} пересека${intersecting.length === 1 ? "ет" : "ют"} участок`
: ""}
</div>
</div>
);
}