feat(site-finder): прогрессивное раскрытие секций отчёта §1–§6 («Раскрыть всё») (#2116)
All checks were successful
Deploy / changes (push) Successful in 8s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Successful in 3m34s
Deploy / deploy (push) Successful in 1m22s

This commit is contained in:
bot-backend 2026-06-30 15:31:33 +00:00
parent 663f04b777
commit e52dc47067
7 changed files with 413 additions and 346 deletions

View file

@ -21,6 +21,7 @@ import { NspdRegulationCard } from "./NspdRegulationCard";
import { NspdVerifyLink } from "./NspdVerifyLink"; import { NspdVerifyLink } from "./NspdVerifyLink";
import { PoiList2Gis } from "./PoiList2Gis"; import { PoiList2Gis } from "./PoiList2Gis";
import { ExportButtons } from "./ExportButtons"; import { ExportButtons } from "./ExportButtons";
import { StageDetails } from "./StageDetails";
import { import {
adaptEgrn, adaptEgrn,
formatEncumbrance, formatEncumbrance,
@ -295,6 +296,7 @@ export function Section1ParcelInfo({ cad }: Props) {
/> />
</div> </div>
<StageDetails>
{/* 2-column grid: map (left) + EGRN + POI (right) */} {/* 2-column grid: map (left) + EGRN + POI (right) */}
<div <div
style={{ style={{
@ -351,6 +353,7 @@ export function Section1ParcelInfo({ cad }: Props) {
> >
<ExportButtons cad={cad} analyzeData={data} egrn={egrn} /> <ExportButtons cad={cad} analyzeData={data} egrn={egrn} />
</div> </div>
</StageDetails>
</section> </section>
); );
} }

View file

@ -44,6 +44,7 @@ import {
type UtilityInfrastructureResponse, type UtilityInfrastructureResponse,
} from "@/hooks/useUtilityInfrastructure"; } from "@/hooks/useUtilityInfrastructure";
import { buildUtilityTableRows, type UtilityTableRow } from "./utility-table"; import { buildUtilityTableRows, type UtilityTableRow } from "./utility-table";
import { StageDetails } from "./StageDetails";
// ── Connection-points map (#1746) ───────────────────────────────────────────── // ── Connection-points map (#1746) ─────────────────────────────────────────────
// Высота тайла карты для drill-in уровня раздела. Легенда + CpLayerControlPanel // Высота тайла карты для drill-in уровня раздела. Легенда + CpLayerControlPanel
@ -770,6 +771,7 @@ export function Section2NetworksUtilities({ cad }: Props) {
<HeadlineBar title="Сети" subtitle={headlineSubtitle} /> <HeadlineBar title="Сети" subtitle={headlineSubtitle} />
</div> </div>
<StageDetails>
{/* Power line охранная зона warning */} {/* Power line охранная зона warning */}
{powerLineZone && ( {powerLineZone && (
<div <div
@ -794,8 +796,9 @@ export function Section2NetworksUtilities({ cad }: Props) {
/> />
<span> <span>
Участок находится в охранной зоне ЛЭП 35 кВ. Капитальное Участок находится в охранной зоне ЛЭП 35 кВ. Капитальное
строительство в охранной зоне (ОЗ) запрещено (СП 36.13330). Участок строительство в охранной зоне (ОЗ) запрещено (СП 36.13330).
в зоне с особыми условиями использования территорий (ЗОУИТ), тип 5. Участок в зоне с особыми условиями использования территорий
(ЗОУИТ), тип 5.
</span> </span>
</div> </div>
)} )}
@ -876,11 +879,16 @@ export function Section2NetworksUtilities({ cad }: Props) {
<Info <Info
size={14} size={14}
strokeWidth={1.5} strokeWidth={1.5}
style={{ flexShrink: 0, marginTop: 1, color: "var(--fg-tertiary)" }} style={{
flexShrink: 0,
marginTop: 1,
color: "var(--fg-tertiary)",
}}
/> />
<span>{note}</span> <span>{note}</span>
</div> </div>
)} )}
</StageDetails>
</section> </section>
); );
} }

View file

@ -19,6 +19,7 @@ import type {
ParcelAnalysis, ParcelAnalysis,
ParcelAnalysisCompetitor, ParcelAnalysisCompetitor,
} from "@/types/site-finder"; } from "@/types/site-finder";
import { StageDetails } from "./StageDetails";
// ── Types ───────────────────────────────────────────────────────────────────── // ── Types ─────────────────────────────────────────────────────────────────────
@ -799,6 +800,7 @@ export function Section3SettingsAndCompetitors({ cad, data }: Props) {
</div> </div>
</div> </div>
<StageDetails>
{/* Sub-sections */} {/* Sub-sections */}
<div style={{ display: "flex", flexDirection: "column", gap: 24 }}> <div style={{ display: "flex", flexDirection: "column", gap: 24 }}>
<Section31Settings filters={filters} onFiltersChange={setFilters} /> <Section31Settings filters={filters} onFiltersChange={setFilters} />
@ -833,6 +835,7 @@ export function Section3SettingsAndCompetitors({ cad, data }: Props) {
{competitorCount} конкурентов {competitorCount} конкурентов
</div> </div>
)} )}
</StageDetails>
{/* Competitor detail drawer */} {/* Competitor detail drawer */}
<CompetitorDetailDrawer <CompetitorDetailDrawer

View file

@ -26,6 +26,7 @@ import { GeotechRiskBlock } from "@/components/site-finder/GeotechRiskBlock";
import { NspdZouitOverlapsBlock } from "@/components/site-finder/NspdZouitOverlapsBlock"; import { NspdZouitOverlapsBlock } from "@/components/site-finder/NspdZouitOverlapsBlock";
import { SuccessRecommendationBlock } from "@/components/site-finder/SuccessRecommendationBlock"; import { SuccessRecommendationBlock } from "@/components/site-finder/SuccessRecommendationBlock";
import { useParcelAnalyzeQuery } from "@/lib/site-finder-api"; import { useParcelAnalyzeQuery } from "@/lib/site-finder-api";
import { StageDetails } from "./StageDetails";
import type { ParcelAnalysis } from "@/types/site-finder"; import type { ParcelAnalysis } from "@/types/site-finder";
import type { FactorContribution, ScoreGroupTotal } from "@/types/site-finder"; import type { FactorContribution, ScoreGroupTotal } from "@/types/site-finder";
@ -187,6 +188,7 @@ export function Section4Estimate({ cad }: Props) {
</div> </div>
)} )}
<StageDetails>
{/* ── Risk blocks grid ─────────────────────────────────────────────── */} {/* ── Risk blocks grid ─────────────────────────────────────────────── */}
{(analysis.geology || analysis.hydrology || analysis.geotech_risk) && ( {(analysis.geology || analysis.hydrology || analysis.geotech_risk) && (
<div <div
@ -253,6 +255,7 @@ export function Section4Estimate({ cad }: Props) {
/> />
</div> </div>
)} )}
</StageDetails>
</section> </section>
); );
} }

View file

@ -20,6 +20,7 @@ import { Wind, Thermometer, Droplets } from "lucide-react";
import { HeadlineBar } from "@/components/ui/HeadlineBar"; import { HeadlineBar } from "@/components/ui/HeadlineBar";
import { SeasonalWeatherBlock } from "@/components/site-finder/SeasonalWeatherBlock"; import { SeasonalWeatherBlock } from "@/components/site-finder/SeasonalWeatherBlock";
import { StageDetails } from "./StageDetails";
import { useParcelAnalyzeQuery } from "@/lib/site-finder-api"; import { useParcelAnalyzeQuery } from "@/lib/site-finder-api";
import type { ParcelAnalysis } from "@/types/site-finder"; import type { ParcelAnalysis } from "@/types/site-finder";
import type { import type {
@ -625,6 +626,7 @@ export function Section5Atmosphere({ cad }: Props) {
{/* Seasonal weather (climate normals) */} {/* Seasonal weather (climate normals) */}
{hasSeasonal && ( {hasSeasonal && (
<StageDetails>
<div> <div>
<div <div
style={{ style={{
@ -644,6 +646,7 @@ export function Section5Atmosphere({ cad }: Props) {
</div> </div>
<SeasonalWeatherBlock seasonal={analysis.seasonal_weather} /> <SeasonalWeatherBlock seasonal={analysis.seasonal_weather} />
</div> </div>
</StageDetails>
)} )}
</div> </div>
) : ( ) : (

View file

@ -38,6 +38,7 @@ import { ForecastScoringBlock } from "./ForecastScoringBlock";
import { ForecastFutureSupplyBlock } from "./ForecastFutureSupplyBlock"; import { ForecastFutureSupplyBlock } from "./ForecastFutureSupplyBlock";
import { ForecastMetaLine } from "./ForecastMetaLine"; import { ForecastMetaLine } from "./ForecastMetaLine";
import { ForecastExportButtons } from "./ForecastExportButtons"; import { ForecastExportButtons } from "./ForecastExportButtons";
import { StageDetails } from "./StageDetails";
import { import {
CONFIDENCE_RU, CONFIDENCE_RU,
deficitPegState, deficitPegState,
@ -373,6 +374,7 @@ function ForecastReady({
</div> </div>
)} )}
<StageDetails>
{/* Траектория индекса дефицита (chart-then-table, выше 6.1). Причину {/* Траектория индекса дефицита (chart-then-table, выше 6.1). Причину
схлопывания сценариев показываем в 6.2 под заголовком «Почему один схлопывания сценариев показываем в 6.2 под заголовком «Почему один
сценарий, а не три» (#1963) здесь дубль убран. */} сценарий, а не три» (#1963) здесь дубль убран. */}
@ -468,6 +470,7 @@ function ForecastReady({
{ADVISORY_DISCLAIMER} {ADVISORY_DISCLAIMER}
</p> </p>
</div> </div>
</StageDetails>
</div> </div>
</section> </section>
); );

View file

@ -0,0 +1,44 @@
"use client";
import { useState } from "react";
interface StageDetailsProps {
children: React.ReactNode;
openLabel?: string;
closeLabel?: string;
}
export function StageDetails({
children,
openLabel = "Раскрыть всё",
closeLabel = "Свернуть",
}: StageDetailsProps) {
const [open, setOpen] = useState(false);
return (
<>
<button
type="button"
aria-expanded={open}
onClick={() => setOpen((prev) => !prev)}
style={{
width: "100%",
padding: "10px 14px",
border: "1px solid var(--border-card)",
borderRadius: 8,
background: "var(--bg-card-alt)",
color: "var(--accent)",
fontSize: 13,
fontWeight: 500,
cursor: "pointer",
textAlign: "center",
marginTop: 16,
}}
>
{open ? closeLabel : openLabel}
{open ? " ▴" : " ▾"}
</button>
{open && <div style={{ marginTop: 16 }}>{children}</div>}
</>
);
}