diff --git a/frontend/src/components/site-finder/GateVerdictBanner.tsx b/frontend/src/components/site-finder/GateVerdictBanner.tsx new file mode 100644 index 00000000..02e503a2 --- /dev/null +++ b/frontend/src/components/site-finder/GateVerdictBanner.tsx @@ -0,0 +1,190 @@ +"use client"; +import { useState } from "react"; +import type { GateVerdict, GateVerdictLabel } from "@/types/site-finder"; +import { SectionLabel } from "@/components/ui/SectionLabel"; + +interface GateVerdictBannerProps { + verdict: GateVerdict | null | undefined; +} + +interface VerdictColor { + bg: string; + border: string; + icon: string; + iconBg: string; +} + +const COLOR_BY_LABEL: Record = { + Можно: { bg: "#ecfdf5", border: "#34d399", icon: "✅", iconBg: "#10b981" }, + Нельзя: { bg: "#fef2f2", border: "#f87171", icon: "🚫", iconBg: "#ef4444" }, + "С ограничениями": { + bg: "#fffbeb", + border: "#fbbf24", + icon: "⚠️", + iconBg: "#f59e0b", + }, + "Нужна проверка": { + bg: "#f9fafb", + border: "#9ca3af", + icon: "❓", + iconBg: "#6b7280", + }, +}; + +const SOURCE_LABEL: Record = { + nspd_dump: "данные NSPD актуальны", + nspd_dump_partial: "данные NSPD устарели (>180 дней)", + no_data: "данные NSPD не подгружены", +}; + +function blockerCountLabel(n: number): string { + if (n === 1) return "1 блокер"; + if (n >= 2 && n <= 4) return `${n} блокера`; + return `${n} блокеров`; +} + +function warningCountLabel(n: number): string { + if (n === 1) return "1 предупреждение"; + if (n >= 2 && n <= 4) return `${n} предупреждения`; + return `${n} предупреждений`; +} + +export function GateVerdictBanner({ verdict }: GateVerdictBannerProps) { + const [expanded, setExpanded] = useState(false); + + if (!verdict) return null; + + const color = COLOR_BY_LABEL[verdict.verdict_label]; + const hasDetails = verdict.blockers.length > 0 || verdict.warnings.length > 0; + + const sourceHint = + verdict.source === "nspd_dump_partial" + ? "⏱ устарели" + : verdict.source === "no_data" + ? "❓ нет данных" + : "🟢 свежие"; + + const summaryParts: string[] = []; + if (verdict.blockers.length > 0) + summaryParts.push(blockerCountLabel(verdict.blockers.length)); + if (verdict.warnings.length > 0) + summaryParts.push(warningCountLabel(verdict.warnings.length)); + const summaryText = + summaryParts.length > 0 ? summaryParts.join(" · ") : "Без замечаний"; + + return ( +
+ {/* Header row */} +
+
+ {color.icon} +
+
+
+ МКД: {verdict.verdict_label} +
+
+ {summaryText} + {" · "} + + {sourceHint} + +
+
+ {hasDetails && ( + + )} +
+ + {/* Expanded details */} + {expanded && ( +
+ {verdict.blockers.length > 0 && ( +
+ 🚫 Блокеры +
    + {verdict.blockers.map((b) => ( +
  • + {b.code}: {b.detail} +
  • + ))} +
+
+ )} + {verdict.warnings.length > 0 && ( +
+ ⚠️ Предупреждения +
    + {verdict.warnings.map((w) => ( +
  • + {w.code}: {w.detail} +
  • + ))} +
+
+ )} + {verdict.checks_performed.length > 0 && ( +
+ Проверки: {verdict.checks_performed.join(" · ")} +
+ )} +
+ )} +
+ ); +} diff --git a/frontend/src/components/site-finder/OverviewTab.tsx b/frontend/src/components/site-finder/OverviewTab.tsx index 5fd5b245..532c8db4 100644 --- a/frontend/src/components/site-finder/OverviewTab.tsx +++ b/frontend/src/components/site-finder/OverviewTab.tsx @@ -4,6 +4,7 @@ import type { FeatureCollection } from "geojson"; import type { ParcelAnalysis } from "@/types/site-finder"; import { SectionLabel } from "@/components/ui/SectionLabel"; import { ConfidenceBadge } from "./ConfidenceBadge"; +import { GateVerdictBanner } from "./GateVerdictBanner"; import { IsochronesPanel } from "./IsochronesPanel"; import { ScoreBreakdownPanel } from "./ScoreBreakdownPanel"; @@ -40,6 +41,9 @@ export function OverviewTab({ data, onIsochronesResult }: Props) { return (
+ {/* G5 (#32): gate verdict banner — МКД buildability */} + {data.gate_verdict && } + {/* X2 (#48): confidence indicator на самом верху Overview */} {data.confidence !== undefined && data.confidence_label && (