"use client"; /** * Section6Forecast — "6. Прогноз" (958-B3 / §22). * * Renders the async §22 demand/supply forecast, scenarios and confidence on the * Site Finder analysis screen. Self-fetches via useParcelForecastQuery, which * POLLs GET /api/v1/parcels/{cad}/forecast every 4s until status === "ready" * (the forecast is enqueued by useParcelAnalyzeQuery on page mount — render-only * here, no trigger). * * Layout mirrors Section1-5: dark HeadlineBar + section root id="section-6". * exec_summary banner (headline + verdict + KPI row) * 6.1 Прогноз по горизонтам → ForecastHorizonsBlock * 6.2 Сценарии → ScenariosBlock * 6.3 Уверенность → ForecastConfidenceBlock * advisory disclaimer (footer) */ import { LineChart } from "lucide-react"; import { HeadlineBar } from "@/components/ui/HeadlineBar"; import { KpiCard } from "@/components/site-finder/KpiCard"; import { useParcelForecastQuery } from "@/lib/site-finder-api"; import type { ForecastReport } from "@/types/forecast"; import { ForecastHorizonsBlock } from "./ForecastHorizonsBlock"; import { ScenariosBlock } from "./ScenariosBlock"; import { ForecastConfidenceBlock } from "./ForecastConfidenceBlock"; import { CONFIDENCE_RU, deficitVariant, fmtNum } from "./forecast-helpers"; interface Props { cad: string; /** * Horizon chosen in the analysis-screen HorizonSelector (6 / 12 / 18). When * set it takes priority over the report-derived horizon so the user's choice * instantly drives which horizon is treated as the target (client-side), even * before the recomputed forecast finishes (see AnalysisPageContent comment). */ selectedHorizon?: number; } const ADVISORY_DISCLAIMER = "Оценка advisory — не основание для инвест-решения."; const SCROLL_MARGIN = 72; // KpiCard color enum (site-finder variant) mapped from Badge variant words. function kpiColorForDeficit(deficitIndex: number): "green" | "red" | "neutral" { const v = deficitVariant(deficitIndex); if (v === "success") return "green"; if (v === "danger") return "red"; return "neutral"; } // ── Section6Forecast ───────────────────────────────────────────────────────── export function Section6Forecast({ cad, selectedHorizon }: Props) { const { data, isLoading, error } = useParcelForecastQuery(cad); const isPending = isLoading || (data != null && data.status !== "ready") || data == null; // ── Pending: forecast still computing (poll in flight) ───────────────────── if (isPending && !error) { return (
Прогноз рассчитывается…
{[0, 1, 2].map((i) => (
))}
); } // ── Error ────────────────────────────────────────────────────────────────── if (error || !data || data.status !== "ready" || !data.report) { return (
Данные прогноза недоступны
); } return ; } // ── ForecastReady (report present) ─────────────────────────────────────────── function ForecastReady({ report, selectedHorizon, }: { report: ForecastReport; selectedHorizon?: number; }) { const exec = report.exec_summary; const fm = report.future_market; // Target horizon: the user's HorizonSelector choice wins (instant client-side // highlight), else future_supply.horizon_months, else median of meta horizons, // else 12. const targetHorizon = selectedHorizon ?? fm.future_supply?.horizon_months ?? medianHorizon(report.meta.horizons) ?? 12; const kn = exec.key_numbers; const hasHorizons = fm.forecasts_by_horizon.length > 0; const hasScenarios = Object.keys(report.scenarios.by_scenario).length > 0 || fm.scenarios_summary != null; const hasConfidence = report.confidence.level != null || Object.keys(report.confidence.factors).length > 0; const hasAny = hasHorizons || hasScenarios || hasConfidence; // Headline subtitle = future_market summary (one sentence «откуда / что значит»). const subtitle = fm.summary ?? undefined; return (
{/* exec_summary verdict + KPI row */} {exec.verdict && (

{exec.verdict}

)}
0 ? "+" : ""}${fmtNum(kn.deficit_index, 2)}` : "—" } color={ kn.deficit_index != null ? kpiColorForDeficit(kn.deficit_index) : "neutral" } />
{!hasAny && (
Данные прогноза недоступны
)} {/* 6.1 Прогноз по горизонтам */} {hasHorizons && ( )} {/* 6.2 Сценарии */} {hasScenarios && ( )} {/* 6.3 Уверенность */} {hasConfidence && ( )} {/* Advisory disclaimer */}

{ADVISORY_DISCLAIMER}

); } // ── SubBlock wrapper (6.1 / 6.2 / 6.3) ─────────────────────────────────────── function SubBlock({ id, title, children, }: { id: string; title: string; children: React.ReactNode; }) { return (

{title}

{children}
); } // ── Helpers ────────────────────────────────────────────────────────────────── function medianHorizon(horizons: number[]): number | null { if (horizons.length === 0) return null; const sorted = [...horizons].sort((a, b) => a - b); return sorted[Math.floor(sorted.length / 2)]; } function confidenceKpiColor( level: "high" | "medium" | "low" | null, ): "green" | "amber" | "red" | "neutral" { if (level === "high") return "green"; if (level === "medium") return "amber"; if (level === "low") return "red"; return "neutral"; }