"use client"; import { useState } from "react"; import type { ParcelAnalysis, ParcelAnalysisNoise, ParcelAnalysisAirQuality, ParcelAnalysisWind, } from "@/types/site-finder"; import { MarketTrendBlock } from "./MarketTrendBlock"; interface Props { data: ParcelAnalysis; } const CATEGORY_LABELS: Record = { school: "Школы", kindergarten: "Детские сады", pharmacy: "Аптеки", hospital: "Больницы / клиники", shop_mall: "ТЦ / ТРЦ", shop_supermarket: "Супермаркеты", shop_small: "Магазины", park: "Парки", tram_stop: "Трамвайные остановки", bus_stop: "Автобусные остановки", metro_stop: "Метро", }; const SOURCE_TYPE_LABELS: Record = { highway: "Магистраль", railway: "Ж/д", industrial: "Производство", aerodrome: "Аэродром", }; function scoreColor(score: number): string { if (score > 5) return "#16a34a"; if (score >= 2) return "#d97706"; return "#dc2626"; } type ScoreLabel = "плохо" | "средне" | "хорошо" | "отлично"; const SCORE_LABEL_BG: Record = { отлично: "#dcfce7", хорошо: "#dbeafe", средне: "#fef3c7", плохо: "#fecaca", }; const SCORE_LABEL_COLOR: Record = { отлично: "#15803d", хорошо: "#1d4ed8", средне: "#b45309", плохо: "#b91c1c", }; 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); } function noiseBg(score: number): string { if (score >= 0.7) return "#dcfce7"; if (score >= 0.4) return "#fef3c7"; return "#fecaca"; } function pm25Color(v: number): string { if (v < 10) return "#16a34a"; if (v < 25) return "#d97706"; if (v < 50) return "#ea580c"; return "#dc2626"; } function pm10Color(v: number): string { if (v < 20) return "#16a34a"; if (v < 50) return "#d97706"; if (v < 100) return "#ea580c"; return "#dc2626"; } function no2Color(v: number): string { if (v < 40) return "#16a34a"; if (v < 100) return "#d97706"; return "#dc2626"; } function formatTs(iso: string): string { try { return new Date(iso).toLocaleDateString("ru-RU", { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit", }); } catch { return iso; } } // ── Noise block ─────────────────────────────────────────────────────────────── function NoiseBlock({ noise }: { noise: ParcelAnalysisNoise }) { const [open, setOpen] = useState(false); const top3 = noise.sources.slice(0, 3); return (
Шум
~{Math.round(noise.estimated_db)} dB
{noise.level}
{top3.length > 0 && (
{open && (
{top3.map((s, i) => (
{SOURCE_TYPE_LABELS[s.source_type] ?? s.source_type} {s.name ? ` «${s.name}»` : ""} — {Math.round(s.distance_m)} м ({Math.round(s.estimated_db)} dB)
))}
)}
)}
); } // ── Air Quality block ───────────────────────────────────────────────────────── function AirQualityBlock({ aq }: { aq: ParcelAnalysisAirQuality | null }) { if (!aq) { return (
Воздух
Данные недоступны
); } return (
Воздух
{aq.source} · {formatTs(aq.ts)}
); } function AqBadge({ label, value, unit, color, }: { label: string; value: number; unit: string; color: string; }) { return (
{label}
{value.toFixed(1)}
{unit}
); } // ── Wind block ──────────────────────────────────────────────────────────────── function WindArrow({ deg }: { deg: number }) { // Arrow points "up" by default (north). Rotate to show wind coming FROM that direction. return ( {/* compass ring */} {/* arrow group rotated to dominant_direction_deg */} {/* shaft */} {/* arrowhead */} {/* tail notch */} ); } function WindBlock({ wind }: { wind: ParcelAnalysisWind | null }) { if (!wind) { return (
Ветер
Данные недоступны
); } return (
Ветер
{wind.dominant_direction_label} {wind.max_speed_m_s != null ? ` · до ${wind.max_speed_m_s} м/с` : ""}
за {wind.forecast_days} дн.
); } // ── ScoreCard ───────────────────────────────────────────────────────────────── export function ScoreCard({ data }: Props) { const tramPois = data.score_breakdown["tram_stop"] ?? []; const nearestTram = tramPois.length > 0 ? Math.round(Math.min(...tramPois.map((p) => p.distance_m))) : null; const hasEnvironmental = data.noise !== undefined || data.air_quality !== undefined || data.wind !== undefined; return (
{/* Score header */}
{data.score.toFixed(2)}
{/* score_max_reference + label badge */}
{data.score_max_reference !== undefined && ( / {data.score_max_reference.toFixed(0)} )} {data.score_label && ( {data.score_label} )}
Социальный балл участка
{data.poi_count} POI ·{" "} {data.source === "cad_quarter" ? "квартал" : "участок"}
{data.score_explanation && (
{data.score_explanation}
)}
{/* District */} {data.district && (
{data.district.district_name} · медиана {(data.district.median_price_per_m2 / 1000).toFixed(0)}{" "} тыс ₽/м² · {(data.district.dist_to_center / 1000).toFixed(1)} км до центра
)} {/* Tram warning */} {nearestTram !== null && (
Трамвай в {nearestTram} м (возможный источник шума)
)} {/* 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)} м
))}
)}
{/* Environmental factors */} {hasEnvironmental && (
Внешние факторы
{data.noise !== undefined ? ( data.noise !== null ? ( ) : (
Шум
Данные недоступны
) ) : null}
)} {/* Market trend */} {"market_trend" in data && (
)}
); }