"use client"; 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"; import { ScoreBreakdownStackedBar } from "./ScoreBreakdownStackedBar"; interface Props { data: ParcelAnalysis; onIsochronesResult?: (fc: FeatureCollection) => void; } const CATEGORY_LABELS: Record = { school: "Школы", kindergarten: "Детские сады", pharmacy: "Аптеки", hospital: "Больницы / клиники", shop_mall: "ТЦ / ТРЦ", shop_supermarket: "Супермаркеты", shop_small: "Магазины", park: "Парки", tram_stop: "Трамвайные остановки", bus_stop: "Автобусные остановки", metro_stop: "Метро", }; function avgDist(items: Array<{ distance_m: number }>): number { if (!items.length) return 0; return Math.round(items.reduce((s, i) => s + i.distance_m, 0) / items.length); } export function OverviewTab({ data, onIsochronesResult }: Props) { const tramPois = data.score_breakdown["tram_stop"] ?? []; const nearestTram = tramPois.length > 0 ? Math.round(Math.min(...tramPois.map((p) => p.distance_m))) : null; return (
{/* G5 (#32): gate verdict banner — МКД buildability */} {data.gate_verdict && } {/* X2 (#48): confidence indicator на самом верху Overview */} {data.confidence !== undefined && data.confidence_label && ( )} {/* District info */} {data.district && (
Район
{data.district.district_name}
Медиана:{" "} {(data.district.median_price_per_m2 / 1000).toFixed(0)} тыс ₽/м² (12 мес) {(data.district.dist_to_center / 1000).toFixed(1)} км до центра
{data.location && (
До центра ЕКБ:{" "} {data.location.distance_to_center_km.toFixed(1)} км {data.location.center_bonus > 0 && ( (+{data.location.center_bonus.toFixed(1)} к score) )}
)}
)} {/* B5 EGRN compact (added 2026-05-18 — expose new field в legacy Обзор) */} {data.egrn && (
ЕГРН
{data.egrn.address && (
Адрес: {data.egrn.address}
)} {data.egrn.permitted_use_text && (
ВРИ: {data.egrn.permitted_use_text}
)} {data.egrn.land_category && (
Категория: {data.egrn.land_category}
)} {data.egrn.parcel_status && (
Статус: {data.egrn.parcel_status}
)} {data.egrn.right_type && (
Право: {data.egrn.right_type}
)}
)} {/* B5 Utilities summary (added 2026-05-18 — expose new field в legacy Обзор) */} {data.utilities && Array.isArray(data.utilities.summary) && data.utilities.summary.length > 0 && (
Сети рядом
{data.utilities.summary .map((u) => { const label = u.subtype === "substation" ? "электричество" : u.subtype === "power_line" ? "ЛЭП" : u.subtype === "pipeline" ? "газ" : u.subtype === "water_intake" ? "вода" : u.subtype === "pumping_station" ? "канализация" : u.subtype; return `${label} ${u.nearest_m} м`; }) .join(" · ")}
{data.utilities.power_line_охранная_зона_flag && (
⚠ Охранная зона ЛЭП — капитальное строительство запрещено
)}
)} {/* Tram warning */} {nearestTram !== null && (
Трамвай в {nearestTram} м — возможный источник шума
)} {/* Score details */}
Балл
{data.poi_count} POI ·{" "} {data.source === "cad_quarter" ? "квартал" : "участок"}
{data.score_explanation && (
{data.score_explanation}
)} {data.score_without_center !== undefined && (
Без бонуса центра: {data.score_without_center.toFixed(2)}
)}
{/* X1 (#47): per-factor score breakdown с verbal explain */} {data.score_breakdown_detailed && data.score_breakdown_detailed.length > 0 && ( )} {/* #114: score breakdown stacked bar by category */} {data.score_breakdown_detailed && data.score_breakdown_detailed.length > 0 && ( )} {/* POI breakdown */}
POI по категориям {Object.entries(data.score_breakdown).length === 0 ? (

Нет данных

) : (
{Object.entries(data.score_breakdown).map(([cat, items]) => (
{CATEGORY_LABELS[cat] ?? cat} {items.length} шт · ср. {avgDist(items)} м
))}
)}
{/* Isochrones */} {data.isochrones_available === false ? (
Изохроны доступности
Недоступны — нужен OpenRouteService API key (см.{" "} backend/.env.runtime )
) : data.isochrones_available === true && onIsochronesResult ? (
) : null}
); }