"use client"; /** * Section5Atmosphere — "5. Атмосфера / воздух" * * Layout: * HeadlineBar (title "5. Атмосфера" + subtitle summary) * Responsive grid repeat(auto-fit, minmax(220px, 1fr)): * NoiseBlock — conditional on analyze.noise * AirQualityBlock — conditional on analyze.air_quality * WindBlock — conditional on analyze.wind / analyze.weather.wind * SeasonalWeatherBlock — conditional on analyze.seasonal_weather * Graceful empty state if all fields null/absent * * Data: useParcelAnalyzeQuery (canonical cache key ["parcel-analyze", cad]). * No duplicate queries — TanStack shares cache with other sections. */ import { Wind, Thermometer, Droplets } from "lucide-react"; import { HeadlineBar } from "@/components/ui/HeadlineBar"; import { SeasonalWeatherBlock } from "@/components/site-finder/SeasonalWeatherBlock"; import { useParcelAnalyzeQuery } from "@/lib/site-finder-api"; import type { ParcelAnalysis } from "@/types/site-finder"; import type { ParcelAnalysisNoise, ParcelAnalysisAirQuality, ParcelAnalysisWind, } from "@/types/site-finder"; // ── Props ───────────────────────────────────────────────────────────────────── interface Props { cad: string; } // ── Helpers ─────────────────────────────────────────────────────────────────── const NOISE_SOURCE_LABELS: Record = { highway: "Магистраль", railway: "Ж/д", industrial: "Производство", aerodrome: "Аэродром", }; function noiseBg(score: number): string { if (score >= 0.7) return "var(--success-soft, #DCFCE7)"; if (score >= 0.4) return "var(--warn-soft, #FEF3C7)"; return "var(--danger-soft, #FEE2E2)"; } function pm25Color(v: number): string { if (v < 10) return "var(--success, #0A7A3A)"; if (v < 25) return "var(--warn, #9A6700)"; if (v < 50) return "#ea580c"; return "var(--danger, #B3261E)"; } function pm10Color(v: number): string { if (v < 20) return "var(--success, #0A7A3A)"; if (v < 50) return "var(--warn, #9A6700)"; if (v < 100) return "#ea580c"; return "var(--danger, #B3261E)"; } function no2Color(v: number): string { if (v < 40) return "var(--success, #0A7A3A)"; if (v < 100) return "var(--warn, #9A6700)"; return "var(--danger, #B3261E)"; } function uvColor(uv: number): string { if (uv < 3) return "var(--success, #0A7A3A)"; if (uv < 6) return "var(--warn, #9A6700)"; if (uv < 8) return "#ea580c"; return "var(--danger, #B3261E)"; } function formatTs(iso: string): string { try { return new Date(iso).toLocaleDateString("ru-RU", { day: "2-digit", month: "2-digit", year: "numeric", }); } catch { return iso; } } // ── WindArrow SVG ───────────────────────────────────────────────────────────── function WindArrow({ deg }: { deg: number }) { return ( ); } // ── NoiseBlock ──────────────────────────────────────────────────────────────── function NoiseBlock({ noise }: { noise: ParcelAnalysisNoise }) { return (
Шум
~{Math.round(noise.estimated_db)} dB
{noise.level}
{noise.sources.length > 0 && (
Источники ({noise.sources.length})
{noise.sources.slice(0, 5).map((s, i) => (
{NOISE_SOURCE_LABELS[s.source_type] ?? s.source_type} {s.name ? ` «${s.name}»` : ""} — {Math.round(s.distance_m)} м ( {Math.round(s.estimated_db)} dB)
))}
)}
); } // ── AirQualityBlock ─────────────────────────────────────────────────────────── function AqBadge({ label, value, unit, color, }: { label: string; value: number; unit: string; color: string; }) { return (
{label}
{value.toFixed(1)}
{unit}
); } function AirQualityBlock({ aq }: { aq: ParcelAnalysisAirQuality }) { return (
Воздух
{aq.source} · {formatTs(aq.ts)}
); } // ── WindBlock ───────────────────────────────────────────────────────────────── function WindBlock({ wind }: { wind: ParcelAnalysisWind }) { return (
Ветер
{wind.dominant_direction_label} {wind.max_speed_m_s != null ? ` · до ${wind.max_speed_m_s} м/с` : ""}
за {wind.forecast_days} дн.
); } // ── WeatherWindBlock (from weather.wind when no standalone wind field) ──────── function WeatherWindBlock({ wind, uvMax, precipMm, precipDays, forecastDays, source, }: { wind: ParcelAnalysisWind; uvMax: number | null | undefined; precipMm: number; precipDays: number; forecastDays: number; source: string; }) { return (
Погода / Ветер
{wind.dominant_direction_label} {wind.max_speed_m_s != null ? ` · до ${wind.max_speed_m_s} м/с` : ""}
{precipMm} мм · {precipDays} дн. с осадками за {forecastDays} дн.
{uvMax != null && (
UV макс:  {uvMax}
)}
{source} · {forecastDays} дн.
); } // ── Empty-state card ────────────────────────────────────────────────────────── function EmptyCard({ label }: { label: string }) { return (
{label}
Данные недоступны
); } // ── Headline helpers ────────────────────────────────────────────────────────── function buildHeadlineTitle(analysis: ParcelAnalysis): string { const parts: string[] = []; if (analysis.noise != null) { const level = analysis.noise.level; parts.push(`Шум: ${level}`); } if (analysis.air_quality != null) { const pm = analysis.air_quality.pm2_5.toFixed(1); parts.push(`PM2.5: ${pm} мкг/м³`); } if (parts.length === 0) return "5. Атмосфера"; return `5. Атмосфера · ${parts.join(" · ")}`; } function buildHeadlineSubtitle(analysis: ParcelAnalysis): string | undefined { const parts: string[] = []; if (analysis.wind != null) { parts.push(`Ветер: ${analysis.wind.dominant_direction_label}`); } else if (analysis.weather?.wind != null) { parts.push(`Ветер: ${analysis.weather.wind.dominant_direction_label}`); } if (analysis.seasonal_weather != null) { parts.push(`Климат: ${analysis.seasonal_weather.period}`); } if (parts.length === 0) return undefined; return parts.join(" · "); } // ── Section5Atmosphere ──────────────────────────────────────────────────────── export function Section5Atmosphere({ cad }: Props) { const { data, isLoading, error } = useParcelAnalyzeQuery(cad); // ── Loading skeleton ─────────────────────────────────────────────────────── if (isLoading) { return (
{[0, 1, 2].map((i) => (
))}
); } // ── Error state ──────────────────────────────────────────────────────────── if (error || !data) { return (
{error instanceof Error ? error.message : "Ошибка загрузки данных"}
); } // Cast to ParcelAnalysis (shared cache with other sections) const analysis = data as unknown as ParcelAnalysis; const hasNoise = analysis.noise != null; const hasAirQuality = analysis.air_quality != null; const hasWind = analysis.wind != null; const hasWeather = analysis.weather != null; const hasSeasonal = analysis.seasonal_weather != null; const hasAny = hasNoise || hasAirQuality || hasWind || hasWeather || hasSeasonal; const headlineTitle = buildHeadlineTitle(analysis); const headlineSubtitle = buildHeadlineSubtitle(analysis); return (
{/* HeadlineBar */} {hasAny ? (
{/* Primary blocks grid */}
{/* Noise */} {hasNoise ? ( ) : ( )} {/* Air quality */} {hasAirQuality ? ( ) : ( )} {/* Wind — prefer standalone wind, fall back to weather.wind */} {hasWind ? ( ) : hasWeather && analysis.weather!.wind != null ? ( ) : ( )}
{/* Seasonal weather (climate normals) */} {hasSeasonal && (
Климат (нормали 30 лет)
)}
) : ( /* Full empty state — all fields null */
Данные об атмосфере недоступны для этого участка
)}
); }