From e52dc4706757f44324c5d2ccdd18cddf42dd5c6f Mon Sep 17 00:00:00 2001 From: bot-backend Date: Tue, 30 Jun 2026 15:31:33 +0000 Subject: [PATCH] =?UTF-8?q?feat(site-finder):=20=D0=BF=D1=80=D0=BE=D0=B3?= =?UTF-8?q?=D1=80=D0=B5=D1=81=D1=81=D0=B8=D0=B2=D0=BD=D0=BE=D0=B5=20=D1=80?= =?UTF-8?q?=D0=B0=D1=81=D0=BA=D1=80=D1=8B=D1=82=D0=B8=D0=B5=20=D1=81=D0=B5?= =?UTF-8?q?=D0=BA=D1=86=D0=B8=D0=B9=20=D0=BE=D1=82=D1=87=D1=91=D1=82=D0=B0?= =?UTF-8?q?=20=C2=A71=E2=80=93=C2=A76=20(=C2=AB=D0=A0=D0=B0=D1=81=D0=BA?= =?UTF-8?q?=D1=80=D1=8B=D1=82=D1=8C=20=D0=B2=D1=81=D1=91=C2=BB)=20(#2116)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../analysis/Section1ParcelInfo.tsx | 101 ++++---- .../analysis/Section2NetworksUtilities.tsx | 224 +++++++++--------- .../Section3SettingsAndCompetitors.tsx | 65 ++--- .../site-finder/analysis/Section4Estimate.tsx | 123 +++++----- .../analysis/Section5Atmosphere.tsx | 39 +-- .../site-finder/analysis/Section6Forecast.tsx | 163 ++++++------- .../site-finder/analysis/StageDetails.tsx | 44 ++++ 7 files changed, 413 insertions(+), 346 deletions(-) create mode 100644 frontend/src/components/site-finder/analysis/StageDetails.tsx diff --git a/frontend/src/components/site-finder/analysis/Section1ParcelInfo.tsx b/frontend/src/components/site-finder/analysis/Section1ParcelInfo.tsx index 5535fbb7..890f0969 100644 --- a/frontend/src/components/site-finder/analysis/Section1ParcelInfo.tsx +++ b/frontend/src/components/site-finder/analysis/Section1ParcelInfo.tsx @@ -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) { /> - {/* 2-column grid: map (left) + EGRN + POI (right) */} -
- {/* Left: mini-map */} - + + {/* 2-column grid: map (left) + EGRN + POI (right) */} +
+ {/* Left: mini-map */} + - {/* Right: EGRN table + НСПД-градрегламент + POI list */} -
- + {/* Right: EGRN table + НСПД-градрегламент + POI list */} +
+ - {/* #1962 — реальный ПЗЗ-градрегламент из nspd_zoning (synth #1891). + {/* #1962 — реальный ПЗЗ-градрегламент из nspd_zoning (synth #1891). Раньше отчёт показывал только мёртвый zoning.data_available=false (закрытый PKK6-путь) и НЕ выводил резолвленный регламент. КСИТ-ёмкость считается от площади ЕГРН (egrn.area_m2). */} - 0 - ? egrn.area_m2 - : null - } - /> - - {poiData ? ( - 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. - - )} -
-
- {/* Export buttons */} -
- -
+ {poiData ? ( + + ) : ( + // 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. + + )} +
+
+ + {/* Export buttons */} +
+ +
+ ); } diff --git a/frontend/src/components/site-finder/analysis/Section2NetworksUtilities.tsx b/frontend/src/components/site-finder/analysis/Section2NetworksUtilities.tsx index 09bd4b9d..341f593a 100644 --- a/frontend/src/components/site-finder/analysis/Section2NetworksUtilities.tsx +++ b/frontend/src/components/site-finder/analysis/Section2NetworksUtilities.tsx @@ -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) { - {/* Power line охранная зона warning */} - {powerLineZone && ( -
- - - Участок находится в охранной зоне ЛЭП ≥35 кВ. Капитальное - строительство в охранной зоне (ОЗ) запрещено (СП 36.13330). Участок - в зоне с особыми условиями использования территорий (ЗОУИТ), тип 5. - -
- )} + + {/* Power line охранная зона warning */} + {powerLineZone && ( +
+ + + Участок находится в охранной зоне ЛЭП ≥35 кВ. Капитальное + строительство в охранной зоне (ОЗ) запрещено (СП 36.13330). + Участок в зоне с особыми условиями использования территорий + (ЗОУИТ), тип 5. + +
+ )} - {/* Счётчики источников — явно разделяем «инж.сети OSM» и «точки + {/* Счётчики источников — явно разделяем «инж.сети OSM» и «точки подключения НСПД», чтобы числа не путались (#1953). */} - {!networksLoading && ( - - )} - - {/* Utilities table — строки из osm_utility_infrastructure (= карта) */} - {networksLoading ? ( -
- Загрузка инженерных сетей... -
- ) : tableRows.length > 0 ? ( - - ) : ( -
- - Инженерные сети (OSM) не найдены в радиусе {COVERAGE_RADIUS_KM} км. - {connectionPointCount > 0 - ? " Точки подключения (НСПД) показаны на карте ниже." - : ""} -
- )} - - {/* Карта точек подключения + охранных зон сетей + инж.сети OSM (#1746) */} - - - {/* Caveat note */} - {note && ( -
- - {note} -
- )} + )} + + {/* Utilities table — строки из osm_utility_infrastructure (= карта) */} + {networksLoading ? ( +
+ Загрузка инженерных сетей... +
+ ) : tableRows.length > 0 ? ( + + ) : ( +
+ + Инженерные сети (OSM) не найдены в радиусе {COVERAGE_RADIUS_KM} км. + {connectionPointCount > 0 + ? " Точки подключения (НСПД) показаны на карте ниже." + : ""} +
+ )} + + {/* Карта точек подключения + охранных зон сетей + инж.сети OSM (#1746) */} + + + {/* Caveat note */} + {note && ( +
+ + {note} +
+ )} +
); } diff --git a/frontend/src/components/site-finder/analysis/Section3SettingsAndCompetitors.tsx b/frontend/src/components/site-finder/analysis/Section3SettingsAndCompetitors.tsx index 4390e44b..3ea221c0 100644 --- a/frontend/src/components/site-finder/analysis/Section3SettingsAndCompetitors.tsx +++ b/frontend/src/components/site-finder/analysis/Section3SettingsAndCompetitors.tsx @@ -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) { - {/* Sub-sections */} -
- + + {/* Sub-sections */} +
+ - {/* Competitor table — moved before 3.2/3.3 for context */} - {filteredCompetitors.length > 0 && ( - - )} + {/* Competitor table — moved before 3.2/3.3 for context */} + {filteredCompetitors.length > 0 && ( + + )} - - -
+ + +
- {/* Filtered competitors count hint */} - {competitorCount > 0 && - filteredCompetitors.length !== competitorCount && ( -
- Фильтр по радиусу: показано {filteredCompetitors.length} из{" "} - {competitorCount} конкурентов -
- )} + {/* Filtered competitors count hint */} + {competitorCount > 0 && + filteredCompetitors.length !== competitorCount && ( +
+ Фильтр по радиусу: показано {filteredCompetitors.length} из{" "} + {competitorCount} конкурентов +
+ )} + {/* Competitor detail drawer */} )} - {/* ── Risk blocks grid ─────────────────────────────────────────────── */} - {(analysis.geology || analysis.hydrology || analysis.geotech_risk) && ( -
- {analysis.geology && } - {analysis.hydrology && ( - - )} - {analysis.geotech_risk && ( - - )} -
- )} - - {/* ── NSPD ZOUIT overlaps ──────────────────────────────────────────── */} - {hasZouit && ( -
+ + {/* ── Risk blocks grid ─────────────────────────────────────────────── */} + {(analysis.geology || analysis.hydrology || analysis.geotech_risk) && (
- Охранные зоны ЗОУИТ + {analysis.geology && } + {analysis.hydrology && ( + + )} + {analysis.geotech_risk && ( + + )}
- -
- )} + )} - {/* ── Success recommendation ───────────────────────────────────────── */} - {analysis.success_recommendation !== undefined && ( -
- -
- )} + {/* ── NSPD ZOUIT overlaps ──────────────────────────────────────────── */} + {hasZouit && ( +
+
+ Охранные зоны ЗОУИТ +
+ +
+ )} + + {/* ── Success recommendation ───────────────────────────────────────── */} + {analysis.success_recommendation !== undefined && ( +
+ +
+ )} + ); } diff --git a/frontend/src/components/site-finder/analysis/Section5Atmosphere.tsx b/frontend/src/components/site-finder/analysis/Section5Atmosphere.tsx index cd4b5f23..bfaa31e5 100644 --- a/frontend/src/components/site-finder/analysis/Section5Atmosphere.tsx +++ b/frontend/src/components/site-finder/analysis/Section5Atmosphere.tsx @@ -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 && ( -
-
- - Климат (нормали 30 лет) + +
+
+ + Климат (нормали 30 лет) +
+
- -
+ )}
) : ( diff --git a/frontend/src/components/site-finder/analysis/Section6Forecast.tsx b/frontend/src/components/site-finder/analysis/Section6Forecast.tsx index 8d8a7070..f008c495 100644 --- a/frontend/src/components/site-finder/analysis/Section6Forecast.tsx +++ b/frontend/src/components/site-finder/analysis/Section6Forecast.tsx @@ -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({ )} - {/* Траектория индекса дефицита (chart-then-table, выше 6.1). Причину + + {/* Траектория индекса дефицита (chart-then-table, выше 6.1). Причину схлопывания сценариев показываем в 6.2 под заголовком «Почему один сценарий, а не три» (#1963) — здесь дубль убран. */} - + - {/* 6.1 Прогноз по горизонтам */} - {hasHorizons && ( - - - - )} + {/* 6.1 Прогноз по горизонтам */} + {hasHorizons && ( + + + + )} - {/* 6.2 Сценарии */} - {hasScenarios && ( - - - - )} + {/* 6.2 Сценарии */} + {hasScenarios && ( + + + + )} - {/* 6.3 Уверенность */} - {hasConfidence && ( - - - - )} + {/* 6.3 Уверенность */} + {hasConfidence && ( + + + + )} - {/* 6.4 Рекомендация по продукту — что строить на участке (§13.4) */} - {hasProductTz && pt && ( - - - - )} + {/* 6.4 Рекомендация по продукту — что строить на участке (§13.4) */} + {hasProductTz && pt && ( + + + + )} - {/* 6.4a Прогноз по всем форматам — таблица ассортимента (#1745, фаза 1). + {/* 6.4a Прогноз по всем форматам — таблица ассортимента (#1745, фаза 1). Под рекомендацией: весь срез дефицита по форматам рекомендованного класса. Показываем только когда есть форматные ячейки квартирографии. */} - {hasAssortment && pt && ( - - - - )} + {hasAssortment && pt && ( + + + + )} - {/* 6.5 Прозрачность скоринга — почему итоговый скор такой (§13.6) */} - {hasScoring && sc && ( - - - - )} + {/* 6.5 Прозрачность скоринга — почему итоговый скор такой (§13.6) */} + {hasScoring && sc && ( + + + + )} - {/* 6.6 Будущее предложение и конкуренты — §16-evidence за прогнозом (§13.3) */} - {hasFutureSupplyDetail && ( - - - - )} + {/* 6.6 Будущее предложение и конкуренты — §16-evidence за прогнозом (§13.3) */} + {hasFutureSupplyDetail && ( + + + + )} - {/* Freshness / traceability: когда рассчитан + горизонты + версия схемы */} -
- - - {/* Advisory disclaimer (всегда последним) */} -

- {ADVISORY_DISCLAIMER} -

-
+ + + {/* Advisory disclaimer (всегда последним) */} +

+ {ADVISORY_DISCLAIMER} +

+ +
); diff --git a/frontend/src/components/site-finder/analysis/StageDetails.tsx b/frontend/src/components/site-finder/analysis/StageDetails.tsx new file mode 100644 index 00000000..e31f6a60 --- /dev/null +++ b/frontend/src/components/site-finder/analysis/StageDetails.tsx @@ -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 ( + <> + + {open &&
{children}
} + + ); +}