"use client"; import { useState } from "react"; import type { ParcelAnalysis, ParcelAnalysisNoise, ParcelAnalysisAirQuality, ParcelAnalysisWind, ParcelAnalysisWeather, } from "@/types/site-finder"; import { MarketTrendBlock } from "./MarketTrendBlock"; import { GeologyBlock } from "./GeologyBlock"; import { SeasonalWeatherBlock } from "./SeasonalWeatherBlock"; import { HydrologyBlock } from "./HydrologyBlock"; import { GeotechRiskBlock } from "./GeotechRiskBlock"; 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} дн.
); } // ── Weather block ───────────────────────────────────────────────────────────── function uvColor(uv: number): string { if (uv < 3) return "#16a34a"; if (uv < 6) return "#d97706"; if (uv < 8) return "#ea580c"; return "#dc2626"; } function WeatherBlock({ weather, }: { weather: ParcelAnalysisWeather | null | undefined; }) { if (!weather) { return (
Погода / Климат
Прогноз недоступен
); } const { temperature: t, wind } = weather; return (
Погода / Климат
{/* Temperature */}
Температура
{t.min_c != null && t.max_c != null ? `${t.min_c}…${t.max_c}°C` : t.min_c != null ? `от ${t.min_c}°C` : t.max_c != null ? `до ${t.max_c}°C` : "—"}
{(t.avg_min_c != null || t.avg_max_c != null) && (
ср. {t.avg_min_c != null ? `${t.avg_min_c}` : "?"} {"/"} {t.avg_max_c != null ? `${t.avg_max_c}` : "?"} °C
)}
{/* Precipitation */}
Осадки
{weather.precipitation_total_mm} мм {" "} (за {weather.forecast_days} дн.)
{weather.precipitation_days} дн. с осадками
{/* UV */} {weather.uv_index_max != null && (
UV макс:
{weather.uv_index_max}
)} {/* Wind */}
{wind.dominant_direction_label} {wind.max_speed_m_s != null ? ` · до ${wind.max_speed_m_s} м/с` : ""}
{/* Source + note */}
{weather.source} ·{" "} {weather.note.length > 60 ? weather.note.slice(0, 60) + "…" : weather.note}
); } // ── 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 || data.weather !== 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} {data.weather !== undefined ? ( ) : ( )}
)} {/* Market trend */} {"market_trend" in data && (
)} {/* Seasonal weather */} {"seasonal_weather" in data && (
)} {/* Hydrology */} {data.hydrology !== undefined && (
)} {/* Geotech risk */} {data.geotech_risk !== undefined && (
)} {/* Geology */} {data.geology !== undefined && (
Геология
)}
); }