"use client"; import { useState } from "react"; import Link from "next/link"; import { useQueryClient } from "@tanstack/react-query"; import { ChatPanel } from "@/components/site-finder/ChatPanel"; import { GateVerdictBanner } from "@/components/site-finder/GateVerdictBanner"; import { HorizonSelector } from "@/components/site-finder/HorizonSelector"; import { Section1ParcelInfo } from "@/components/site-finder/analysis/Section1ParcelInfo"; import { Section2NetworksUtilities } from "@/components/site-finder/analysis/Section2NetworksUtilities"; import { Section3SettingsAndCompetitors } from "@/components/site-finder/analysis/Section3SettingsAndCompetitors"; import { Section4Estimate } from "@/components/site-finder/analysis/Section4Estimate"; import { Section5Atmosphere } from "@/components/site-finder/analysis/Section5Atmosphere"; import { Section6Forecast } from "@/components/site-finder/analysis/Section6Forecast"; import { Section7Concept } from "@/components/site-finder/analysis/Section7Concept"; import { useParcelAnalyzeQuery } from "@/lib/site-finder-api"; import type { ParcelAnalysis } from "@/types/site-finder"; // ── Props ───────────────────────────────────────────────────────────────────── interface Props { cad: string; } // ── Page Content (client — needs TanStack Query) ─────────────────────────────── export function AnalysisPageContent({ cad }: Props) { const [horizon, setHorizon] = useState(12); const queryClient = useQueryClient(); const { data, isLoading, isFetching, error } = useParcelAnalyzeQuery( cad, horizon, ); // Changing the horizon re-runs /analyze (expensive ~10-30s), which re-enqueues // the async §22 forecast (recompute ~30-180s). Invalidating ["parcel-forecast", // cad] forces Section 6's poll to re-fetch; until the new run is ready it may // still show the previous run's emphasis. The selectedHorizon prop passed to // Section6Forecast gives an instant client-side target highlight regardless — // that's intended. function handleHorizonChange(h: number) { setHorizon(h); void queryClient.invalidateQueries({ queryKey: ["parcel-forecast", cad] }); } // ── Loading ──────────────────────────────────────────────────────────────── if (isLoading) { return (
Анализируем участок {cad}…
); } // ── Error ────────────────────────────────────────────────────────────────── if (error || !data) { const msg = error instanceof Error ? error.message : "Ошибка загрузки анализа"; return (
{msg}
Вернуться к Site Finder
); } // ── Results layout ───────────────────────────────────────────────────────── // Cast to ParcelAnalysis (superset of ParcelAnalyzeResponse) — both describe // the same /analyze endpoint; Section3 requires the richer type. const analysis = data as unknown as ParcelAnalysis; const districtName = analysis.district?.district_name ?? "—"; const areaHa = analysis.geometry_suitability?.area_ha; const areaStr = areaHa != null ? `${areaHa.toFixed(2)} га` : null; return (
{/* Breadcrumb */} {/* Controls row — horizon selector drives the analyze + forecast queries */}
{/* Page layout: sidebar (240px) + main scroll. На планшете/телефоне .gd-grid-sidebar схлопывает грид в одну колонку (issue #66). */}
{/* ── Sidebar nav ───────────────────────────────────────────────── */} {/* ── Main scroll area ──────────────────────────────────────────── */}
{/* Вердикт-баннер участка — выше всех секций (#1741). */} {/* ── Группа «Участок» ──────────────────────────────────────── */} {/* 1. Информация — IMPLEMENTED in A5 */} {/* 2. Оценка участка — IMPLEMENTED in A10 */} {/* 3. Сети и точки подключения — IMPLEMENTED in A6 */} {/* ── Группа «Стройка и рынок» ──────────────────────────────── */} {/* 4. Рынок и конкуренты — IMPLEMENTED in A7 */} {/* 5. Атмосфера — IMPLEMENTED in A11 */} {/* Статус пересчёта прогноза при смене горизонта (#1744) — нейтральная серая строка вместо молчаливого disabled. */} {isFetching && (
Пересчитываем прогноз на {horizon} мес…
)} {/* 6. Прогноз и рекомендация — IMPLEMENTED in 958-B3 (§22 forecast) */} {/* 7. Концепция — генеративный дизайн застройки (#1965 Stage 1). Seeded из анализа (geom + financial_estimate), считается по запросу; result lazy-mounted (Leaflet). */} {/* Chat — grounded parcel-chat over the §22 forecast (#958) */}
); } // ── Page heading (h1 a11y) ───────────────────────────────────────────────────── // // Единственный

страницы анализа (#1926 a11y): присутствует во ВСЕХ состояниях // (loading / error / success), не только в success — иначе SR-пользователь на этапе // загрузки/ошибки остаётся без заголовка верхнего уровня. district дописывается // когда известен (success). Стиль = h1-токен (22/28 weight 600, left-align). function PageHeading({ cad, districtName, }: { cad: string; districtName?: string; }) { const hasDistrict = districtName != null && districtName !== "—"; return (

Анализ участка {cad} {hasDistrict ? ` · ${districtName}` : ""}

); } // ── Breadcrumb ───────────────────────────────────────────────────────────────── function Breadcrumb({ cad, districtName, areaStr, }: { cad: string; districtName?: string; areaStr?: string | null; }) { const parts = [cad]; if (districtName && districtName !== "—") parts.push(districtName); if (areaStr) parts.push(areaStr); return (
Site Finder / {parts.map((part, i) => ( {i > 0 && ( · )} {part} ))} · анализ
); } // ── Group divider ──────────────────────────────────────────────────────────── // // Плашка-разделитель между смысловыми группами секций (#1741): тёмный фон // --bg-headline + светлый uppercase-label --fg-on-dark, паттерн headline-bar. function GroupDivider({ label }: { label: string }) { return (
{label}
); } // ── Sidebar nav ──────────────────────────────────────────────────────────────── // // Две группы с групп-хедерами (#1741). Нумерация секций сквозная 1–6 под новый // порядок: «Участок» → 1-3, «Стройка и рынок» → 4-6. interface NavItem { id: string; label: string; children?: { id: string; label: string }[]; } interface NavGroup { label: string; items: NavItem[]; } const NAV_GROUPS: NavGroup[] = [ { label: "Участок", items: [ { id: "section-1", label: "1. Информация" }, { id: "section-4", label: "2. Оценка участка" }, { id: "section-2", label: "3. Сети и точки подключения" }, ], }, { label: "Стройка и рынок", items: [ { id: "section-3", label: "4. Рынок и конкуренты", children: [ { id: "section-3-1", label: "4.1 Настройки выборки" }, { id: "section-3-2", label: "4.2 Планировки" }, { id: "section-3-3", label: "4.3 Остатки и скорость" }, ], }, { id: "section-5", label: "5. Атмосфера" }, { id: "section-6", label: "6. Прогноз и рекомендация", children: [ { id: "section-6-1", label: "6.1 Прогноз по горизонтам" }, { id: "section-6-2", label: "6.2 Сценарии" }, { id: "section-6-3", label: "6.3 Уверенность" }, { id: "section-6-4", label: "6.4 Рекомендация по продукту" }, { id: "section-6-5", label: "6.5 Прозрачность скоринга" }, { id: "section-6-6", label: "6.6 Будущее предложение и конкуренты" }, ], }, { id: "section-7", label: "7. Концепция" }, ], }, ]; function AnalysisSidebarNav() { function scrollTo(id: string) { const el = document.getElementById(id); if (el) el.scrollIntoView({ behavior: "smooth" }); } return ( ); } // ── Section placeholder ──────────────────────────────────────────────────────── function SectionPlaceholder({ number, title, note, }: { number: string; title: string; note: string; }) { return ( <>
{number}. {title}
{note}
); }