gendesign/frontend/src/app/site-finder/analysis/[cad]/ptica/PticaPageContent.tsx
Light1YT e18bb06539 feat(ptica): build Отчёты + Сравнение cockpit tabs (PR#3)
Reports: dark export cards reusing real infra — PDF snapshot (/snapshot.pdf
via shared API_BASE_URL+triggerDownload), client-side CSV (formula-injection
guard + UTF-8 BOM preserved), §22 forecast DOCX/PPTX gated on forecast-ready
with honest 404; full-report DOCX/PPTX disabled «скоро» (no endpoint). Section
checklist mirrors the snapshot TOC.

Compare: reuses compare-shortlist (localStorage, COMPARE_MAX) + ParcelCompareLoader
for real per-cad analyze metrics (shared cache, rules-of-hooks safe via keyed
loader). Current cad auto-pinned + non-removable, validated кадастр input,
removable chips. Dark comparison table with winner highlight + colour-coded
verdict (Можно/С огранич./Нельзя).

Dark styles scoped under .pticaRoot[data-theme=dark]; reuse-only (ExportButtons,
compare page, loaders untouched); no new deps. TS strict, no any.
2026-06-20 15:58:50 +05:00

102 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
/**
* PticaPageContent — client root of the ПТИЦА cockpit (INCREMENT 1).
*
* Wires the SAME /analyze data as the existing light analysis page via
* useParcelAnalyzeQuery, narrows the response to ParcelAnalysis (the sanctioned
* `as unknown as` pattern — see AnalysisPageContent.tsx:111), and composes the
* dark operator-terminal shell. Only the 'analysis' tab renders content in PR#1;
* Scenarios / Reports / Compare show an honest "В разработке" panel.
*
* The root carries BOTH `.pticaRoot` and `data-theme="dark"` so the scoped dark
* tokens apply here and never leak to the light pages.
*/
import { useState } from "react";
import Link from "next/link";
import styles from "./ptica.module.css";
import {
useParcelAnalyzeQuery,
useParcelForecastQuery,
} from "@/lib/site-finder-api";
import type { ParcelAnalysis } from "@/types/site-finder";
import { PticaShell } from "@/components/site-finder/ptica/PticaShell";
import type { PticaTab } from "@/components/site-finder/ptica/PticaShell";
import { PticaHero } from "@/components/site-finder/ptica/PticaHero";
import { DevelopmentScan } from "@/components/site-finder/ptica/DevelopmentScan";
import { PticaScenarios } from "@/components/site-finder/ptica/scenarios/PticaScenarios";
import { PticaReports } from "@/components/site-finder/ptica/reports/PticaReports";
import { PticaCompare } from "@/components/site-finder/ptica/compare/PticaCompare";
interface Props {
cad: string;
}
export function PticaPageContent({ cad }: Props) {
const [horizon, setHorizon] = useState<number>(12);
const [tab, setTab] = useState<PticaTab>("analysis");
const { data, isLoading, error } = useParcelAnalyzeQuery(cad, horizon);
// §22 forecast is keyed/cached by cad — call unconditionally; it polls the
// async report (enqueued by /analyze) and feeds both the hero invest-score and
// the Scenarios tab. Pending state is handled inside each consumer.
const { data: forecastData } = useParcelForecastQuery(cad);
const forecastReport =
forecastData?.status === "ready" ? forecastData.report : undefined;
// ── Loading ────────────────────────────────────────────────────────────────
if (isLoading) {
return (
<div className={styles.pticaRoot} data-theme="dark">
<div className={styles.stateScreen}>
<div className={styles.stateBox}>Анализируем участок {cad}</div>
</div>
</div>
);
}
// ── Error ──────────────────────────────────────────────────────────────────
if (error || !data) {
const msg =
error instanceof Error ? error.message : "Ошибка загрузки анализа";
return (
<div className={styles.pticaRoot} data-theme="dark">
<div className={styles.stateScreen}>
<div>
<div className={styles.stateError}>{msg}</div>
<Link href="/site-finder" className={styles.stateLink}>
Вернуться к Site Finder
</Link>
</div>
</div>
</div>
);
}
// Narrow to ParcelAnalysis (superset of ParcelAnalyzeResponse) — both describe
// the same /analyze endpoint. Sanctioned pattern (AnalysisPageContent.tsx:111).
const analysis = data as unknown as ParcelAnalysis;
return (
<div className={styles.pticaRoot} data-theme="dark">
<PticaShell activeTab={tab} onTabChange={setTab}>
{tab === "analysis" && (
<>
<PticaHero analysis={analysis} forecastReport={forecastReport} />
<DevelopmentScan analysis={analysis} />
</>
)}
{tab === "scenarios" && (
<PticaScenarios
cad={cad}
horizon={horizon}
onHorizonChange={setHorizon}
/>
)}
{tab === "reports" && <PticaReports cad={cad} analysis={analysis} />}
{tab === "compare" && <PticaCompare cad={cad} />}
</PticaShell>
</div>
);
}