gendesign/frontend/src/components/site-finder/NspdZouitOverlapsBlock.tsx
Light1YT 6adba229f0
All checks were successful
CI / changes (pull_request) Successful in 6s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Successful in 51s
CI / openapi-codegen-check (pull_request) Successful in 1m45s
fix(site-finder): wire encumbrance/obj_class/ЗОУИТ display + forecast axis & flat-state (#1953)
Four frontend display fixes for the Site Finder analysis report (audit #1953);
all consume fields the (already-deployed) backend now returns.

- #1954: ЕГРН «Обременения» row was hardcoded «—». Wire top-level
  `encumbrance` block {has_zouit, zouit_count, zouit_types} via
  formatEncumbrance → «ЗОУИТ: N (types)» / «не выявлено (НСПД)».
- #1955: show obj_class per success bucket («Студии 15-30 · Комфорт») so
  rows with identical area-labels are distinct; fix top-row match + React key
  to (bucket, obj_class).
- #1957: NspdZouitOverlapsBlock renders human type_zone + reg_numb_border,
  adds a СЗЗ caveat banner (СанПиН 2.2.1/2.1.1.1200-03) and «СЗЗ вашего
  участка» highlight; switched raw hex to UI tokens.
- #1958: ForecastChart axis names → nameLocation:middle + nameGap (no legend
  overlap); grey flat-state callout when the deficit line is degenerate
  (scenarios_collapsed or all points clamped at ±1).

Tests: formatEncumbrance + isDeficitDegenerate (15 new). Real segment
differentiation is backend #1959 (separate).
2026-06-27 16:51:40 +05:00

239 lines
8.4 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 { AlertTriangle } from "lucide-react";
import type { NspdZouitOverlap } from "@/types/nspd";
interface Props {
overlaps: NspdZouitOverlap[];
/**
* #1957 — кадастровый номер анализируемого участка. Нужен, чтобы подсветить
* «СЗЗ вашего участка» (когда описание/рег-номер охранной зоны ссылается на
* сам участок). Optional: вызовы без cad просто не делают подсветку.
*/
parcelCad?: string;
}
// Group key → human-readable label (fallback, когда type_zone отсутствует).
const GROUP_LABELS: Record<string, string> = {
engineering: "Инженерные коммуникации",
okn: "Объекты культурного наследия",
natural: "Природные территории",
protected: "Охраняемые зоны",
other: "Прочие ЗОУИТ",
};
// #1957 — СЗЗ (санитарно-защитная зона): размещение жилья внутри ограничено/
// запрещено (СанПиН 2.2.1/2.1.1.1200-03), это материальный инвест-риск.
function isSzz(typeZone: string | null | undefined): boolean {
return (typeZone ?? "").toLowerCase().includes("санитарно-защитн");
}
// Severity → токенные цвета (раньше тут был сырой hex, вне ui-tokens).
function getSeverityStyle(
groupKey: string,
szz: boolean,
): { bg: string; color: string; label: string } {
// СЗЗ — всегда danger вне зависимости от group_key (материальный риск).
if (szz)
return { bg: "var(--danger-soft)", color: "var(--danger)", label: "СЗЗ" };
switch (groupKey) {
case "engineering":
return { bg: "var(--warn-soft)", color: "var(--warn)", label: "warn" };
case "okn":
case "protected":
return {
bg: "var(--danger-soft)",
color: "var(--danger)",
label: "block",
};
case "natural":
return { bg: "var(--accent-soft)", color: "var(--accent)", label: "low" };
default:
return {
bg: "var(--bg-card-alt)",
color: "var(--fg-secondary)",
label: "info",
};
}
}
/**
* #1957 — ссылается ли охранная зона на сам участок? Сверяем cad участка с
* descr / name_by_doc / reg_numb_border / name. (СЗЗ собственного предприятия
* участка — «СЗЗ вашего участка», а не чужая зона по соседству.)
*/
function referencesOwnParcel(
overlap: NspdZouitOverlap,
parcelCad: string | undefined,
): boolean {
if (!parcelCad) return false;
const raw = overlap.raw_props ?? {};
const haystacks = [
overlap.reg_numb_border,
overlap.name,
typeof raw.descr === "string" ? raw.descr : null,
typeof raw.name_by_doc === "string" ? raw.name_by_doc : null,
];
return haystacks.some((h) => typeof h === "string" && h.includes(parcelCad));
}
export function NspdZouitOverlapsBlock({ overlaps, parcelCad }: Props) {
if (overlaps.length === 0) {
return (
<div
style={{
display: "flex",
alignItems: "center",
gap: 8,
background: "var(--success-soft)",
border: "1px solid var(--success)",
borderRadius: 8,
padding: "10px 14px",
}}
>
<span
style={{
background: "var(--bg-card)",
color: "var(--success)",
borderRadius: 6,
padding: "2px 10px",
fontSize: 12,
fontWeight: 600,
}}
>
Нет пересечений
</span>
<span style={{ fontSize: 13, color: "var(--success)" }}>
Охранных зон ЗОУИТ не обнаружено
</span>
</div>
);
}
// #1957 — СЗЗ-caveat: показываем, если хоть один overlap — санитарно-защитная
// зона. Если эта СЗЗ ссылается на сам участок — формулировка строже.
const hasSzz = overlaps.some((o) => isSzz(o.type_zone) || isSzz(o.name));
const ownSzz = overlaps.some(
(o) =>
(isSzz(o.type_zone) || isSzz(o.name)) &&
referencesOwnParcel(o, parcelCad),
);
return (
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
{hasSzz && (
<div
role="alert"
style={{
display: "flex",
alignItems: "flex-start",
gap: 10,
padding: "12px 16px",
background: "var(--danger-soft)",
border: "1px solid var(--danger)",
borderRadius: 10,
fontSize: 13,
color: "var(--danger)",
}}
>
<AlertTriangle
size={16}
strokeWidth={1.5}
style={{ flexShrink: 0, marginTop: 1 }}
/>
<span>
{ownSzz
? "Участок попадает в санитарно-защитную зону (СЗЗ вашего участка). "
: "Участок пересекает санитарно-защитную зону (СЗЗ). "}
Размещение жилой застройки в границах СЗЗ ограничено и, как правило,
не допускается (СанПиН 2.2.1/2.1.1.1200-03). Это материальный
инвест-риск: до проектирования нужно подтвердить возможность
строительства жилья на участке.
</span>
</div>
)}
{overlaps.map((overlap, idx) => {
const szz = isSzz(overlap.type_zone) || isSzz(overlap.name);
const style = getSeverityStyle(overlap.group_key, szz);
const own = szz && referencesOwnParcel(overlap, parcelCad);
// #1957 — заголовок строки: человекочитаемая категория зоны (type_zone),
// иначе name, иначе fallback на group-label. type_zone приоритетнее
// общей метки группы («Охраняемые зоны»).
const title =
overlap.type_zone ||
overlap.name ||
GROUP_LABELS[overlap.group_key] ||
overlap.group_key;
const reg = overlap.reg_numb_border;
return (
<div
key={idx}
style={{
background: "var(--bg-card-alt)",
border: "1px solid var(--border-card)",
borderRadius: 8,
padding: "10px 14px",
display: "flex",
alignItems: "flex-start",
gap: 10,
}}
>
<span
style={{
background: style.bg,
color: style.color,
borderRadius: 5,
padding: "2px 8px",
fontSize: 11,
fontWeight: 700,
whiteSpace: "nowrap",
flexShrink: 0,
textTransform: "uppercase",
}}
>
{style.label}
</span>
<div>
<div
style={{
fontSize: 13,
fontWeight: 500,
color: "var(--fg-primary)",
}}
>
{title}
{own && (
<span
style={{
marginLeft: 8,
fontSize: 11,
fontWeight: 700,
color: "var(--danger)",
}}
>
СЗЗ вашего участка
</span>
)}
</div>
{reg && (
<div
style={{
fontSize: 12,
color: "var(--fg-tertiary)",
marginTop: 2,
fontVariantNumeric: "tabular-nums",
}}
>
Рег. номер границы: {reg}
</div>
)}
</div>
</div>
);
})}
</div>
);
}