feat(site-finder): прогрессивное раскрытие секций отчёта §1–§6 («Раскрыть всё») (#2116)
This commit is contained in:
parent
663f04b777
commit
e52dc47067
7 changed files with 413 additions and 346 deletions
|
|
@ -21,6 +21,7 @@ import { NspdRegulationCard } from "./NspdRegulationCard";
|
|||
import { NspdVerifyLink } from "./NspdVerifyLink";
|
||||
import { PoiList2Gis } from "./PoiList2Gis";
|
||||
import { ExportButtons } from "./ExportButtons";
|
||||
import { StageDetails } from "./StageDetails";
|
||||
import {
|
||||
adaptEgrn,
|
||||
formatEncumbrance,
|
||||
|
|
@ -295,62 +296,64 @@ export function Section1ParcelInfo({ cad }: Props) {
|
|||
/>
|
||||
</div>
|
||||
|
||||
{/* 2-column grid: map (left) + EGRN + POI (right) */}
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "400px 1fr",
|
||||
gap: 16,
|
||||
alignItems: "start",
|
||||
marginBottom: 20,
|
||||
}}
|
||||
>
|
||||
{/* Left: mini-map */}
|
||||
<MiniMap data={data} />
|
||||
<StageDetails>
|
||||
{/* 2-column grid: map (left) + EGRN + POI (right) */}
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "400px 1fr",
|
||||
gap: 16,
|
||||
alignItems: "start",
|
||||
marginBottom: 20,
|
||||
}}
|
||||
>
|
||||
{/* Left: mini-map */}
|
||||
<MiniMap data={data} />
|
||||
|
||||
{/* Right: EGRN table + НСПД-градрегламент + POI list */}
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
|
||||
<EgrnPropertyTable data={egrn} />
|
||||
{/* Right: EGRN table + НСПД-градрегламент + POI list */}
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
|
||||
<EgrnPropertyTable data={egrn} />
|
||||
|
||||
{/* #1962 — реальный ПЗЗ-градрегламент из nspd_zoning (synth #1891).
|
||||
{/* #1962 — реальный ПЗЗ-градрегламент из nspd_zoning (synth #1891).
|
||||
Раньше отчёт показывал только мёртвый zoning.data_available=false
|
||||
(закрытый PKK6-путь) и НЕ выводил резолвленный регламент. КСИТ-ёмкость
|
||||
считается от площади ЕГРН (egrn.area_m2). */}
|
||||
<NspdRegulationCard
|
||||
zoning={data.nspd_zoning}
|
||||
areaM2={
|
||||
Number.isFinite(egrn.area_m2) && egrn.area_m2 > 0
|
||||
? egrn.area_m2
|
||||
: null
|
||||
}
|
||||
/>
|
||||
|
||||
{poiData ? (
|
||||
<PoiList2Gis
|
||||
items={poiData.items}
|
||||
totalScore={poiData.poi_weighted_score}
|
||||
<NspdRegulationCard
|
||||
zoning={data.nspd_zoning}
|
||||
areaM2={
|
||||
Number.isFinite(egrn.area_m2) && egrn.area_m2 > 0
|
||||
? egrn.area_m2
|
||||
: null
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
// Fallback: B6 poi-score (useParcelPoiScoreQuery) ещё грузится или
|
||||
// упал. score_breakdown несёт только name/distance_m — без реального
|
||||
// per-POI weight и без POI-weighted-score, поэтому НЕ подставляем
|
||||
// инвест-балл участка (data.score) как POI-оценку «X / 100» и НЕ
|
||||
// фабрикуем weight/score_contribution (#1466). Показываем честный
|
||||
// пустой стейт «POI данные недоступны» до прихода B6.
|
||||
<PoiList2Gis items={[]} totalScore={0} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Export buttons */}
|
||||
<div
|
||||
style={{
|
||||
paddingTop: 16,
|
||||
borderTop: "1px solid var(--border-soft)",
|
||||
}}
|
||||
>
|
||||
<ExportButtons cad={cad} analyzeData={data} egrn={egrn} />
|
||||
</div>
|
||||
{poiData ? (
|
||||
<PoiList2Gis
|
||||
items={poiData.items}
|
||||
totalScore={poiData.poi_weighted_score}
|
||||
/>
|
||||
) : (
|
||||
// Fallback: B6 poi-score (useParcelPoiScoreQuery) ещё грузится или
|
||||
// упал. score_breakdown несёт только name/distance_m — без реального
|
||||
// per-POI weight и без POI-weighted-score, поэтому НЕ подставляем
|
||||
// инвест-балл участка (data.score) как POI-оценку «X / 100» и НЕ
|
||||
// фабрикуем weight/score_contribution (#1466). Показываем честный
|
||||
// пустой стейт «POI данные недоступны» до прихода B6.
|
||||
<PoiList2Gis items={[]} totalScore={0} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Export buttons */}
|
||||
<div
|
||||
style={{
|
||||
paddingTop: 16,
|
||||
borderTop: "1px solid var(--border-soft)",
|
||||
}}
|
||||
>
|
||||
<ExportButtons cad={cad} analyzeData={data} egrn={egrn} />
|
||||
</div>
|
||||
</StageDetails>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ import {
|
|||
type UtilityInfrastructureResponse,
|
||||
} from "@/hooks/useUtilityInfrastructure";
|
||||
import { buildUtilityTableRows, type UtilityTableRow } from "./utility-table";
|
||||
import { StageDetails } from "./StageDetails";
|
||||
|
||||
// ── Connection-points map (#1746) ─────────────────────────────────────────────
|
||||
// Высота тайла карты для drill-in уровня раздела. Легенда + CpLayerControlPanel
|
||||
|
|
@ -770,117 +771,124 @@ export function Section2NetworksUtilities({ cad }: Props) {
|
|||
<HeadlineBar title="Сети" subtitle={headlineSubtitle} />
|
||||
</div>
|
||||
|
||||
{/* Power line охранная зона warning */}
|
||||
{powerLineZone && (
|
||||
<div
|
||||
role="alert"
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "flex-start",
|
||||
gap: 10,
|
||||
padding: "12px 16px",
|
||||
background: "var(--danger-soft)",
|
||||
border: "1px solid var(--danger)",
|
||||
borderRadius: 10,
|
||||
marginBottom: 16,
|
||||
fontSize: 13,
|
||||
color: "var(--danger)",
|
||||
}}
|
||||
>
|
||||
<AlertTriangle
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
style={{ flexShrink: 0, marginTop: 1 }}
|
||||
/>
|
||||
<span>
|
||||
Участок находится в охранной зоне ЛЭП ≥35 кВ. Капитальное
|
||||
строительство в охранной зоне (ОЗ) запрещено (СП 36.13330). Участок
|
||||
в зоне с особыми условиями использования территорий (ЗОУИТ), тип 5.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<StageDetails>
|
||||
{/* Power line охранная зона warning */}
|
||||
{powerLineZone && (
|
||||
<div
|
||||
role="alert"
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "flex-start",
|
||||
gap: 10,
|
||||
padding: "12px 16px",
|
||||
background: "var(--danger-soft)",
|
||||
border: "1px solid var(--danger)",
|
||||
borderRadius: 10,
|
||||
marginBottom: 16,
|
||||
fontSize: 13,
|
||||
color: "var(--danger)",
|
||||
}}
|
||||
>
|
||||
<AlertTriangle
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
style={{ flexShrink: 0, marginTop: 1 }}
|
||||
/>
|
||||
<span>
|
||||
Участок находится в охранной зоне ЛЭП ≥35 кВ. Капитальное
|
||||
строительство в охранной зоне (ОЗ) запрещено (СП 36.13330).
|
||||
Участок в зоне с особыми условиями использования территорий
|
||||
(ЗОУИТ), тип 5.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Счётчики источников — явно разделяем «инж.сети OSM» и «точки
|
||||
{/* Счётчики источников — явно разделяем «инж.сети OSM» и «точки
|
||||
подключения НСПД», чтобы числа не путались (#1953). */}
|
||||
{!networksLoading && (
|
||||
<SourceCounters
|
||||
utilityCount={utilityMappableCount}
|
||||
connectionPointCount={connectionPointCount}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Utilities table — строки из osm_utility_infrastructure (= карта) */}
|
||||
{networksLoading ? (
|
||||
<div
|
||||
style={{
|
||||
height: 160,
|
||||
background: "var(--bg-card-alt)",
|
||||
borderRadius: 10,
|
||||
border: "1px solid var(--border-card)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
color: "var(--fg-tertiary)",
|
||||
fontSize: 13,
|
||||
}}
|
||||
>
|
||||
Загрузка инженерных сетей...
|
||||
</div>
|
||||
) : tableRows.length > 0 ? (
|
||||
<UtilitiesTable rows={tableRows} />
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
padding: "16px",
|
||||
background: "var(--bg-card-alt)",
|
||||
border: "1px dashed var(--border-strong)",
|
||||
borderRadius: 10,
|
||||
color: "var(--fg-tertiary)",
|
||||
fontSize: 13,
|
||||
}}
|
||||
>
|
||||
<Info size={16} strokeWidth={1.5} style={{ flexShrink: 0 }} />
|
||||
Инженерные сети (OSM) не найдены в радиусе {COVERAGE_RADIUS_KM} км.
|
||||
{connectionPointCount > 0
|
||||
? " Точки подключения (НСПД) показаны на карте ниже."
|
||||
: ""}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Карта точек подключения + охранных зон сетей + инж.сети OSM (#1746) */}
|
||||
<ConnectionPointsMap
|
||||
data={data}
|
||||
connectionPoints={connectionPoints}
|
||||
utilityInfrastructure={utilityInfrastructure}
|
||||
isLoading={networksLoading}
|
||||
/>
|
||||
|
||||
{/* Caveat note */}
|
||||
{note && (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "flex-start",
|
||||
gap: 8,
|
||||
marginTop: 12,
|
||||
padding: "10px 12px",
|
||||
background: "var(--bg-card-alt)",
|
||||
borderRadius: 8,
|
||||
fontSize: 12,
|
||||
color: "var(--fg-secondary)",
|
||||
}}
|
||||
>
|
||||
<Info
|
||||
size={14}
|
||||
strokeWidth={1.5}
|
||||
style={{ flexShrink: 0, marginTop: 1, color: "var(--fg-tertiary)" }}
|
||||
{!networksLoading && (
|
||||
<SourceCounters
|
||||
utilityCount={utilityMappableCount}
|
||||
connectionPointCount={connectionPointCount}
|
||||
/>
|
||||
<span>{note}</span>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{/* Utilities table — строки из osm_utility_infrastructure (= карта) */}
|
||||
{networksLoading ? (
|
||||
<div
|
||||
style={{
|
||||
height: 160,
|
||||
background: "var(--bg-card-alt)",
|
||||
borderRadius: 10,
|
||||
border: "1px solid var(--border-card)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
color: "var(--fg-tertiary)",
|
||||
fontSize: 13,
|
||||
}}
|
||||
>
|
||||
Загрузка инженерных сетей...
|
||||
</div>
|
||||
) : tableRows.length > 0 ? (
|
||||
<UtilitiesTable rows={tableRows} />
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
padding: "16px",
|
||||
background: "var(--bg-card-alt)",
|
||||
border: "1px dashed var(--border-strong)",
|
||||
borderRadius: 10,
|
||||
color: "var(--fg-tertiary)",
|
||||
fontSize: 13,
|
||||
}}
|
||||
>
|
||||
<Info size={16} strokeWidth={1.5} style={{ flexShrink: 0 }} />
|
||||
Инженерные сети (OSM) не найдены в радиусе {COVERAGE_RADIUS_KM} км.
|
||||
{connectionPointCount > 0
|
||||
? " Точки подключения (НСПД) показаны на карте ниже."
|
||||
: ""}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Карта точек подключения + охранных зон сетей + инж.сети OSM (#1746) */}
|
||||
<ConnectionPointsMap
|
||||
data={data}
|
||||
connectionPoints={connectionPoints}
|
||||
utilityInfrastructure={utilityInfrastructure}
|
||||
isLoading={networksLoading}
|
||||
/>
|
||||
|
||||
{/* Caveat note */}
|
||||
{note && (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "flex-start",
|
||||
gap: 8,
|
||||
marginTop: 12,
|
||||
padding: "10px 12px",
|
||||
background: "var(--bg-card-alt)",
|
||||
borderRadius: 8,
|
||||
fontSize: 12,
|
||||
color: "var(--fg-secondary)",
|
||||
}}
|
||||
>
|
||||
<Info
|
||||
size={14}
|
||||
strokeWidth={1.5}
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
marginTop: 1,
|
||||
color: "var(--fg-tertiary)",
|
||||
}}
|
||||
/>
|
||||
<span>{note}</span>
|
||||
</div>
|
||||
)}
|
||||
</StageDetails>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import type {
|
|||
ParcelAnalysis,
|
||||
ParcelAnalysisCompetitor,
|
||||
} from "@/types/site-finder";
|
||||
import { StageDetails } from "./StageDetails";
|
||||
|
||||
// ── Types ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -799,40 +800,42 @@ export function Section3SettingsAndCompetitors({ cad, data }: Props) {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sub-sections */}
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 24 }}>
|
||||
<Section31Settings filters={filters} onFiltersChange={setFilters} />
|
||||
<StageDetails>
|
||||
{/* Sub-sections */}
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 24 }}>
|
||||
<Section31Settings filters={filters} onFiltersChange={setFilters} />
|
||||
|
||||
{/* Competitor table — moved before 3.2/3.3 for context */}
|
||||
{filteredCompetitors.length > 0 && (
|
||||
<CompetitorTable
|
||||
competitors={filteredCompetitors}
|
||||
districtName={districtName}
|
||||
onRowClick={setSelectedCompetitor}
|
||||
/>
|
||||
)}
|
||||
{/* Competitor table — moved before 3.2/3.3 for context */}
|
||||
{filteredCompetitors.length > 0 && (
|
||||
<CompetitorTable
|
||||
competitors={filteredCompetitors}
|
||||
districtName={districtName}
|
||||
onRowClick={setSelectedCompetitor}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Section32Layouts cad={cad} />
|
||||
<Section33MarketSales data={data} />
|
||||
</div>
|
||||
<Section32Layouts cad={cad} />
|
||||
<Section33MarketSales data={data} />
|
||||
</div>
|
||||
|
||||
{/* Filtered competitors count hint */}
|
||||
{competitorCount > 0 &&
|
||||
filteredCompetitors.length !== competitorCount && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: 16,
|
||||
padding: "10px 14px",
|
||||
background: "var(--warn-soft, #FEF3C7)",
|
||||
borderRadius: 8,
|
||||
fontSize: 12,
|
||||
color: "var(--warn, #9A6700)",
|
||||
}}
|
||||
>
|
||||
Фильтр по радиусу: показано {filteredCompetitors.length} из{" "}
|
||||
{competitorCount} конкурентов
|
||||
</div>
|
||||
)}
|
||||
{/* Filtered competitors count hint */}
|
||||
{competitorCount > 0 &&
|
||||
filteredCompetitors.length !== competitorCount && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: 16,
|
||||
padding: "10px 14px",
|
||||
background: "var(--warn-soft, #FEF3C7)",
|
||||
borderRadius: 8,
|
||||
fontSize: 12,
|
||||
color: "var(--warn, #9A6700)",
|
||||
}}
|
||||
>
|
||||
Фильтр по радиусу: показано {filteredCompetitors.length} из{" "}
|
||||
{competitorCount} конкурентов
|
||||
</div>
|
||||
)}
|
||||
</StageDetails>
|
||||
|
||||
{/* Competitor detail drawer */}
|
||||
<CompetitorDetailDrawer
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import { GeotechRiskBlock } from "@/components/site-finder/GeotechRiskBlock";
|
|||
import { NspdZouitOverlapsBlock } from "@/components/site-finder/NspdZouitOverlapsBlock";
|
||||
import { SuccessRecommendationBlock } from "@/components/site-finder/SuccessRecommendationBlock";
|
||||
import { useParcelAnalyzeQuery } from "@/lib/site-finder-api";
|
||||
import { StageDetails } from "./StageDetails";
|
||||
import type { ParcelAnalysis } from "@/types/site-finder";
|
||||
import type { FactorContribution, ScoreGroupTotal } from "@/types/site-finder";
|
||||
|
||||
|
|
@ -187,72 +188,74 @@ export function Section4Estimate({ cad }: Props) {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Risk blocks grid ─────────────────────────────────────────────── */}
|
||||
{(analysis.geology || analysis.hydrology || analysis.geotech_risk) && (
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
|
||||
gap: 12,
|
||||
marginBottom: 12,
|
||||
}}
|
||||
>
|
||||
{analysis.geology && <GeologyBlock geology={analysis.geology} />}
|
||||
{analysis.hydrology && (
|
||||
<HydrologyBlock hydrology={analysis.hydrology} />
|
||||
)}
|
||||
{analysis.geotech_risk && (
|
||||
<GeotechRiskBlock risk={analysis.geotech_risk} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── NSPD ZOUIT overlaps ──────────────────────────────────────────── */}
|
||||
{hasZouit && (
|
||||
<div
|
||||
style={{
|
||||
background: "var(--bg-card)",
|
||||
border: "1px solid var(--border-card)",
|
||||
borderRadius: 12,
|
||||
padding: "16px 20px",
|
||||
marginBottom: 12,
|
||||
}}
|
||||
>
|
||||
<StageDetails>
|
||||
{/* ── Risk blocks grid ─────────────────────────────────────────────── */}
|
||||
{(analysis.geology || analysis.hydrology || analysis.geotech_risk) && (
|
||||
<div
|
||||
title="Зоны с особыми условиями использования территорий — охранные зоны инженерной инфраструктуры (ЛЭП, газопровод и т.д.)"
|
||||
style={{
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
color: "var(--fg-secondary)",
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.04em",
|
||||
marginBottom: 10,
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
|
||||
gap: 12,
|
||||
marginBottom: 12,
|
||||
}}
|
||||
>
|
||||
Охранные зоны ЗОУИТ
|
||||
{analysis.geology && <GeologyBlock geology={analysis.geology} />}
|
||||
{analysis.hydrology && (
|
||||
<HydrologyBlock hydrology={analysis.hydrology} />
|
||||
)}
|
||||
{analysis.geotech_risk && (
|
||||
<GeotechRiskBlock risk={analysis.geotech_risk} />
|
||||
)}
|
||||
</div>
|
||||
<NspdZouitOverlapsBlock
|
||||
overlaps={analysis.nspd_zouit_overlaps ?? []}
|
||||
parcelCad={cad}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{/* ── Success recommendation ───────────────────────────────────────── */}
|
||||
{analysis.success_recommendation !== undefined && (
|
||||
<div
|
||||
style={{
|
||||
background: "var(--bg-card)",
|
||||
border: "1px solid var(--border-card)",
|
||||
borderRadius: 12,
|
||||
padding: "16px 20px",
|
||||
}}
|
||||
>
|
||||
<SuccessRecommendationBlock
|
||||
recommendation={analysis.success_recommendation}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{/* ── NSPD ZOUIT overlaps ──────────────────────────────────────────── */}
|
||||
{hasZouit && (
|
||||
<div
|
||||
style={{
|
||||
background: "var(--bg-card)",
|
||||
border: "1px solid var(--border-card)",
|
||||
borderRadius: 12,
|
||||
padding: "16px 20px",
|
||||
marginBottom: 12,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
title="Зоны с особыми условиями использования территорий — охранные зоны инженерной инфраструктуры (ЛЭП, газопровод и т.д.)"
|
||||
style={{
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
color: "var(--fg-secondary)",
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.04em",
|
||||
marginBottom: 10,
|
||||
}}
|
||||
>
|
||||
Охранные зоны ЗОУИТ
|
||||
</div>
|
||||
<NspdZouitOverlapsBlock
|
||||
overlaps={analysis.nspd_zouit_overlaps ?? []}
|
||||
parcelCad={cad}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Success recommendation ───────────────────────────────────────── */}
|
||||
{analysis.success_recommendation !== undefined && (
|
||||
<div
|
||||
style={{
|
||||
background: "var(--bg-card)",
|
||||
border: "1px solid var(--border-card)",
|
||||
borderRadius: 12,
|
||||
padding: "16px 20px",
|
||||
}}
|
||||
>
|
||||
<SuccessRecommendationBlock
|
||||
recommendation={analysis.success_recommendation}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</StageDetails>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import { Wind, Thermometer, Droplets } from "lucide-react";
|
|||
|
||||
import { HeadlineBar } from "@/components/ui/HeadlineBar";
|
||||
import { SeasonalWeatherBlock } from "@/components/site-finder/SeasonalWeatherBlock";
|
||||
import { StageDetails } from "./StageDetails";
|
||||
import { useParcelAnalyzeQuery } from "@/lib/site-finder-api";
|
||||
import type { ParcelAnalysis } from "@/types/site-finder";
|
||||
import type {
|
||||
|
|
@ -625,25 +626,27 @@ export function Section5Atmosphere({ cad }: Props) {
|
|||
|
||||
{/* Seasonal weather (climate normals) */}
|
||||
{hasSeasonal && (
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 11,
|
||||
fontWeight: 500,
|
||||
letterSpacing: "0.04em",
|
||||
textTransform: "uppercase",
|
||||
color: "var(--fg-secondary, #5B6066)",
|
||||
marginBottom: 8,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
<Thermometer size={12} aria-hidden />
|
||||
Климат (нормали 30 лет)
|
||||
<StageDetails>
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 11,
|
||||
fontWeight: 500,
|
||||
letterSpacing: "0.04em",
|
||||
textTransform: "uppercase",
|
||||
color: "var(--fg-secondary, #5B6066)",
|
||||
marginBottom: 8,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
<Thermometer size={12} aria-hidden />
|
||||
Климат (нормали 30 лет)
|
||||
</div>
|
||||
<SeasonalWeatherBlock seasonal={analysis.seasonal_weather} />
|
||||
</div>
|
||||
<SeasonalWeatherBlock seasonal={analysis.seasonal_weather} />
|
||||
</div>
|
||||
</StageDetails>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import { ForecastScoringBlock } from "./ForecastScoringBlock";
|
|||
import { ForecastFutureSupplyBlock } from "./ForecastFutureSupplyBlock";
|
||||
import { ForecastMetaLine } from "./ForecastMetaLine";
|
||||
import { ForecastExportButtons } from "./ForecastExportButtons";
|
||||
import { StageDetails } from "./StageDetails";
|
||||
import {
|
||||
CONFIDENCE_RU,
|
||||
deficitPegState,
|
||||
|
|
@ -373,101 +374,103 @@ function ForecastReady({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Траектория индекса дефицита (chart-then-table, выше 6.1). Причину
|
||||
<StageDetails>
|
||||
{/* Траектория индекса дефицита (chart-then-table, выше 6.1). Причину
|
||||
схлопывания сценариев показываем в 6.2 под заголовком «Почему один
|
||||
сценарий, а не три» (#1963) — здесь дубль убран. */}
|
||||
<ForecastChart report={report} selectedHorizon={selectedHorizon} />
|
||||
<ForecastChart report={report} selectedHorizon={selectedHorizon} />
|
||||
|
||||
{/* 6.1 Прогноз по горизонтам */}
|
||||
{hasHorizons && (
|
||||
<SubBlock id="section-6-1" title="6.1 Прогноз по горизонтам">
|
||||
<ForecastHorizonsBlock
|
||||
forecasts={fm.forecasts_by_horizon}
|
||||
targetHorizon={targetHorizon}
|
||||
peg={peg}
|
||||
/>
|
||||
</SubBlock>
|
||||
)}
|
||||
{/* 6.1 Прогноз по горизонтам */}
|
||||
{hasHorizons && (
|
||||
<SubBlock id="section-6-1" title="6.1 Прогноз по горизонтам">
|
||||
<ForecastHorizonsBlock
|
||||
forecasts={fm.forecasts_by_horizon}
|
||||
targetHorizon={targetHorizon}
|
||||
peg={peg}
|
||||
/>
|
||||
</SubBlock>
|
||||
)}
|
||||
|
||||
{/* 6.2 Сценарии */}
|
||||
{hasScenarios && (
|
||||
<SubBlock id="section-6-2" title="6.2 Сценарии">
|
||||
<ScenariosBlock
|
||||
scenarios={report.scenarios}
|
||||
scenariosSummary={fm.scenarios_summary}
|
||||
targetHorizon={targetHorizon}
|
||||
peg={peg}
|
||||
/>
|
||||
</SubBlock>
|
||||
)}
|
||||
{/* 6.2 Сценарии */}
|
||||
{hasScenarios && (
|
||||
<SubBlock id="section-6-2" title="6.2 Сценарии">
|
||||
<ScenariosBlock
|
||||
scenarios={report.scenarios}
|
||||
scenariosSummary={fm.scenarios_summary}
|
||||
targetHorizon={targetHorizon}
|
||||
peg={peg}
|
||||
/>
|
||||
</SubBlock>
|
||||
)}
|
||||
|
||||
{/* 6.3 Уверенность */}
|
||||
{hasConfidence && (
|
||||
<SubBlock id="section-6-3" title="6.3 Уверенность">
|
||||
<ForecastConfidenceBlock confidence={report.confidence} />
|
||||
</SubBlock>
|
||||
)}
|
||||
{/* 6.3 Уверенность */}
|
||||
{hasConfidence && (
|
||||
<SubBlock id="section-6-3" title="6.3 Уверенность">
|
||||
<ForecastConfidenceBlock confidence={report.confidence} />
|
||||
</SubBlock>
|
||||
)}
|
||||
|
||||
{/* 6.4 Рекомендация по продукту — что строить на участке (§13.4) */}
|
||||
{hasProductTz && pt && (
|
||||
<SubBlock id="section-6-4" title="6.4 Рекомендация по продукту">
|
||||
<ForecastProductTzBlock productTz={pt} />
|
||||
</SubBlock>
|
||||
)}
|
||||
{/* 6.4 Рекомендация по продукту — что строить на участке (§13.4) */}
|
||||
{hasProductTz && pt && (
|
||||
<SubBlock id="section-6-4" title="6.4 Рекомендация по продукту">
|
||||
<ForecastProductTzBlock productTz={pt} />
|
||||
</SubBlock>
|
||||
)}
|
||||
|
||||
{/* 6.4a Прогноз по всем форматам — таблица ассортимента (#1745, фаза 1).
|
||||
{/* 6.4a Прогноз по всем форматам — таблица ассортимента (#1745, фаза 1).
|
||||
Под рекомендацией: весь срез дефицита по форматам рекомендованного
|
||||
класса. Показываем только когда есть форматные ячейки квартирографии. */}
|
||||
{hasAssortment && pt && (
|
||||
<SubBlock id="section-6-4a" title="6.4 Прогноз по всем форматам">
|
||||
<ForecastAssortmentBlock objClass={pt.obj_class} mix={pt.mix} />
|
||||
</SubBlock>
|
||||
)}
|
||||
{hasAssortment && pt && (
|
||||
<SubBlock id="section-6-4a" title="6.4 Прогноз по всем форматам">
|
||||
<ForecastAssortmentBlock objClass={pt.obj_class} mix={pt.mix} />
|
||||
</SubBlock>
|
||||
)}
|
||||
|
||||
{/* 6.5 Прозрачность скоринга — почему итоговый скор такой (§13.6) */}
|
||||
{hasScoring && sc && (
|
||||
<SubBlock id="section-6-5" title="6.5 Прозрачность скоринга">
|
||||
<ForecastScoringBlock scoring={sc} />
|
||||
</SubBlock>
|
||||
)}
|
||||
{/* 6.5 Прозрачность скоринга — почему итоговый скор такой (§13.6) */}
|
||||
{hasScoring && sc && (
|
||||
<SubBlock id="section-6-5" title="6.5 Прозрачность скоринга">
|
||||
<ForecastScoringBlock scoring={sc} />
|
||||
</SubBlock>
|
||||
)}
|
||||
|
||||
{/* 6.6 Будущее предложение и конкуренты — §16-evidence за прогнозом (§13.3) */}
|
||||
{hasFutureSupplyDetail && (
|
||||
<SubBlock
|
||||
id="section-6-6"
|
||||
title="6.6 Будущее предложение и конкуренты"
|
||||
>
|
||||
<ForecastFutureSupplyBlock
|
||||
futureSupply={fm.future_supply}
|
||||
futureCompetitors={fm.future_competitors}
|
||||
/>
|
||||
</SubBlock>
|
||||
)}
|
||||
{/* 6.6 Будущее предложение и конкуренты — §16-evidence за прогнозом (§13.3) */}
|
||||
{hasFutureSupplyDetail && (
|
||||
<SubBlock
|
||||
id="section-6-6"
|
||||
title="6.6 Будущее предложение и конкуренты"
|
||||
>
|
||||
<ForecastFutureSupplyBlock
|
||||
futureSupply={fm.future_supply}
|
||||
futureCompetitors={fm.future_competitors}
|
||||
/>
|
||||
</SubBlock>
|
||||
)}
|
||||
|
||||
{/* Freshness / traceability: когда рассчитан + горизонты + версия схемы */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 6,
|
||||
paddingTop: 12,
|
||||
borderTop: "1px solid var(--border-soft)",
|
||||
}}
|
||||
>
|
||||
<ForecastMetaLine meta={report.meta} runCreatedAt={runCreatedAt} />
|
||||
|
||||
{/* Advisory disclaimer (всегда последним) */}
|
||||
<p
|
||||
{/* Freshness / traceability: когда рассчитан + горизонты + версия схемы */}
|
||||
<div
|
||||
style={{
|
||||
margin: 0,
|
||||
fontSize: 12,
|
||||
lineHeight: 1.5,
|
||||
color: "var(--fg-tertiary)",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 6,
|
||||
paddingTop: 12,
|
||||
borderTop: "1px solid var(--border-soft)",
|
||||
}}
|
||||
>
|
||||
{ADVISORY_DISCLAIMER}
|
||||
</p>
|
||||
</div>
|
||||
<ForecastMetaLine meta={report.meta} runCreatedAt={runCreatedAt} />
|
||||
|
||||
{/* Advisory disclaimer (всегда последним) */}
|
||||
<p
|
||||
style={{
|
||||
margin: 0,
|
||||
fontSize: 12,
|
||||
lineHeight: 1.5,
|
||||
color: "var(--fg-tertiary)",
|
||||
}}
|
||||
>
|
||||
{ADVISORY_DISCLAIMER}
|
||||
</p>
|
||||
</div>
|
||||
</StageDetails>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue