gendesign/frontend/src/components/site-finder/NspdRiskZonesBlock.tsx
bot-backend 5a7d558a5c
All checks were successful
Deploy / changes (push) Successful in 11s
Deploy / deploy-caddy (push) Has been skipped
Deploy / build-backend (push) Successful in 3m10s
Deploy / build-frontend (push) Successful in 4m33s
Deploy / build-worker (push) Successful in 4m42s
Deploy / deploy (push) Successful in 1m50s
Deploy / deploy-status (push) Successful in 2s
Deploy / perimeter-smoke (push) Successful in 13s
fix(ptica): пустой слой риск-зон перестаёт означать «рисков нет» (#2934) (#2940)
2026-08-19 16:34:26 +00:00

198 lines
6.3 KiB
TypeScript
Raw Permalink 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 { RiskZone } from "@/types/nspd";
interface Props {
riskZones: RiskZone[] | null | undefined;
parcelAreaSqm?: number | null;
/**
* #2934: сколько объектов риск-слоёв в дампе квартала. Пустой `riskZones` без
* этого признака неоднозначен, и блок рисовал по нему зелёное «Риски не
* обнаружены» — то есть утверждал результат проверки, которой не было.
*
* `undefined`/`null` — дампа нет; `0` — слой не дал объектов на весь квартал;
* `>0` — слой отработал, и отсутствие пересечений с участком уже настоящий факт.
*/
quarterRisksCount?: number | null;
}
// Severity mapping: layer key suffix → severity tier
function getSeverity(layer: string): "high" | "medium" | "low" {
const key = layer.replace(/^risk_/, "");
if (key === "flooding" || key === "landslide") return "high";
if (key === "flooding_underground" || key === "burns") return "medium";
return "low";
}
const SEVERITY_STYLES: Record<
"high" | "medium" | "low",
{ bg: string; badgeBg: string; color: string; label: string }
> = {
high: {
bg: "#fff1f2",
badgeBg: "#fee2e2",
color: "#991b1b",
label: "ВЫСОКИЙ",
},
medium: {
bg: "#fffbeb",
badgeBg: "#fef3c7",
color: "#92400e",
label: "СРЕДНИЙ",
},
low: {
bg: "#fff7ed",
badgeBg: "#ffedd5",
color: "#9a3412",
label: "НИЗКИЙ",
},
};
function formatArea(sqm: number | null): string | null {
if (sqm === null) return null;
if (sqm >= 10000) return `${(sqm / 10000).toFixed(2)} га`;
return `${Math.round(sqm).toLocaleString("ru-RU")} м²`;
}
export function NspdRiskZonesBlock({
riskZones,
parcelAreaSqm,
quarterRisksCount,
}: Props) {
const zones = riskZones ?? [];
if (zones.length === 0) {
// Зелёный success-стиль оставляем ТОЛЬКО когда слой реально что-то отдал по
// кварталу: тогда «на участке ничего не пересекается» — измеренный факт.
// Замер 19.08: risks_count = 0 у всех 669 дампов, слои risk_* не вернули ни
// одного объекта, — то есть сегодня показывается нейтральная ветка.
const layerAnswered = (quarterRisksCount ?? 0) > 0;
if (!layerAnswered) {
return (
<div
style={{
display: "flex",
alignItems: "center",
gap: 8,
background: "#f8fafc",
border: "1px solid #e2e8f0",
borderRadius: 8,
padding: "10px 14px",
}}
>
<span
style={{
background: "#e2e8f0",
color: "#475569",
borderRadius: 6,
padding: "2px 10px",
fontSize: 12,
fontWeight: 600,
}}
>
Не проверено
</span>
<span style={{ fontSize: 13, color: "#475569" }}>
Слои природного риска НСПД не вернули объектов по этому кварталу
отсутствие риска не подтверждено
</span>
</div>
);
}
return (
<div
style={{
display: "flex",
alignItems: "center",
gap: 8,
background: "#f0fdf4",
border: "1px solid #bbf7d0",
borderRadius: 8,
padding: "10px 14px",
}}
>
<span
style={{
background: "#dcfce7",
color: "#15803d",
borderRadius: 6,
padding: "2px 10px",
fontSize: 12,
fontWeight: 600,
}}
>
Риски не обнаружены
</span>
<span style={{ fontSize: 13, color: "#15803d" }}>
Риск-зоны НСПД на участке не выявлены
</span>
</div>
);
}
// Sort: high first, then medium, then low
const sorted = [...zones].sort((a, b) => {
const order = { high: 0, medium: 1, low: 2 };
return order[getSeverity(a.layer)] - order[getSeverity(b.layer)];
});
return (
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
{sorted.map((zone, idx) => {
const severity = getSeverity(zone.layer);
const style = SEVERITY_STYLES[severity];
const areaStr = formatArea(zone.intersection_area_sqm);
const pct =
parcelAreaSqm && zone.intersection_area_sqm
? Math.min(
100,
Math.round((zone.intersection_area_sqm / parcelAreaSqm) * 100),
)
: null;
return (
<div
key={idx}
style={{
background: style.bg,
border: `1px solid ${style.badgeBg}`,
borderRadius: 8,
padding: "10px 14px",
display: "flex",
alignItems: "flex-start",
gap: 10,
}}
>
<span
style={{
background: style.badgeBg,
color: style.color,
borderRadius: 5,
padding: "2px 8px",
fontSize: 11,
fontWeight: 700,
whiteSpace: "nowrap",
flexShrink: 0,
}}
>
{style.label}
</span>
<div>
<div style={{ fontSize: 13, fontWeight: 500, color: "#1f2937" }}>
{zone.subtype ?? zone.layer}
</div>
{(areaStr || pct !== null) && (
<div style={{ fontSize: 12, color: "#6b7280", marginTop: 2 }}>
{areaStr && <span>Площадь пересечения: {areaStr}</span>}
{areaStr && pct !== null && <span> · </span>}
{pct !== null && <span>{pct}% участка</span>}
</div>
)}
</div>
</div>
);
})}
</div>
);
}