gendesign/frontend/src/components/site-finder/ptica/drawers/DrawerContent.tsx
Light1YT 67b65c8d8a
All checks were successful
CI / frontend-tests (pull_request) Successful in 57s
CI / openapi-codegen-check (pull_request) Successful in 1m49s
CI / changes (pull_request) Successful in 6s
CI / backend-tests (pull_request) Successful in 9m54s
feat(site-finder): bridge финмодели (DCF) в кокпит Investment Clearance (epic #1881 PR-4)
Investment Clearance в site-finder кокпите больше не «—»: синтезируем лёгкий
ТЭП из buildability участка (площадь + предельные параметры зоны НСПД/ПЗЗ —
КСИТ/%застройки/этажность) → прогоняем через полноценную финмодель (каскад
затрат PR-1 + калибр.цена PR-2 + помесячный DCF PR-3) → GDV/Cost/Profit/ROI/
IRR/NPV/PBP в кокпите.

Backend:
- new app/services/site_finder/parcel_financial.py (ЧИСТЫЕ функции, без БД):
  synthesize_teap_from_buildability (GFA=area×max_far, degradation pct+floors;
  residential=GFA×eff; apartments/parking — нормативы teap.py, не дублируются)
  + synthesize_parcel_financial (housing_class из цены, development_type из
  этажей, land_cost=кадастровая). None когда нельзя строить МКД / нет зонинга /
  нет площади.
- parcels.py: 1 вызов в try/except (hot-path-safe → None при сбое) + ключ
  financial_estimate в /analyze. Схема не тронута (AnalyzeResponse extra=allow,
  codegen не нужен). +18 тестов.

Frontend:
- ParcelFinancialEstimate тип (site-finder.ts, hand-typed).
- adaptInvestmentClearance(financial)/adaptFinanceDrawer(financial): реальные
  числа (GDV/Cost/Profit/ROI/IRR + NPV + срок окуп.) или честный placeholder
  когда null. Локализация класса/типа (high_rise→высотная). Прокинут analysis
  через PticaBottomGrid/drawer-registry. +6 тестов, 144 passed.

HEAVY caveat везде: ОРИЕНТИРОВОЧНАЯ модель по МАКС.застройке НСПД, НЕ реальная
концепция; land=кадастровая (не рыночная); график — типовой; IRR proxy-флаг.
mypy strict clean (generative.*), ruff/tsc/lint clean.

Закрывает эпик #1881 (финмодель = полноценная DCF, видна в кокпите).
Follow-up: финансирование (кредит/займы), §22 sales-pace, гео-радиус калибровки.

Refs #1881
2026-06-23 22:44:04 +05:00

905 lines
32 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";
/**
* DrawerContent — per-key detail-drawer bodies, built from the typed drawer
* view-models in ptica-adapt.ts (REAL data first, honest «—» placeholders for
* absent fields). Each export renders the prototype's
* summary → table/sub-tabs → source-line → optional export structure.
*
* Real-vs-placeholder lives entirely in the adapters; these components only
* present the view-models, so a placeholder field is always styled muted with
* its caption rather than shown as a live number.
*/
import styles from "@/app/site-finder/analysis/[cad]/ptica/ptica.module.css";
import type { ParcelAnalysis } from "@/types/site-finder";
import type { ForecastReport } from "@/types/forecast";
import type {
DrawerKvRow,
PticaField,
PticaFactorBar,
} from "@/components/site-finder/ptica/ptica-adapt";
import {
adaptPassportDrawer,
adaptBuildabilityDrawer,
adaptUrbanDrawer,
adaptRestrictionsDrawer,
adaptLegalDrawer,
adaptEngineeringDrawer,
adaptMarketDrawer,
adaptEnvironmentDrawer,
adaptGeorisksDrawer,
adaptRisksDrawer,
adaptProductDrawer,
adaptPotentialDrawer,
adaptEconomyDrawer,
adaptFinanceDrawer,
adaptOksDrawer,
} from "@/components/site-finder/ptica/ptica-adapt";
import { BuildabilityGauge } from "@/components/site-finder/ptica/BuildabilityGauge";
import { MassingViewer } from "@/components/site-finder/ptica/massing/MassingViewer";
import {
DrawerSection,
SummaryBox,
DKvRow,
StatusRow,
DTable,
HBars,
DrawerTabs,
ConfPill,
SourceLine,
DrawerActions,
} from "@/components/site-finder/ptica/drawers/DrawerPrimitives";
/** КСИТ-цель (FAR) — regulation default when НСПД max_far is absent. */
const MASSING_MAX_FAR = 3.5;
/** Fallback parcel area (m²) when geometry_suitability.area_ha is absent. */
const MASSING_FALLBACK_AREA = 6800;
/** Real parcel area in m² from analysis (area_ha × 10000), with fallback. */
function massingAreaM2(analysis: ParcelAnalysis): number {
const areaHa = analysis.geometry_suitability?.area_ha;
if (typeof areaHa === "number" && Number.isFinite(areaHa) && areaHa > 0) {
return areaHa * 10000;
}
return MASSING_FALLBACK_AREA;
}
/** Real КСИТ from НСПД-зонирование (PR #1847), else the regulation default. */
function massingMaxFar(analysis: ParcelAnalysis): number {
const maxFar = analysis.nspd_zoning?.max_far;
if (typeof maxFar === "number" && Number.isFinite(maxFar) && maxFar > 0) {
return maxFar;
}
return MASSING_MAX_FAR;
}
/** True when the КСИТ used by the massing sandbox is the real НСПД value. */
function massingFarIsReal(analysis: ParcelAnalysis): boolean {
const maxFar = analysis.nspd_zoning?.max_far;
return typeof maxFar === "number" && Number.isFinite(maxFar) && maxFar > 0;
}
const SRC_UPDATED = "источник: /analyze";
// ── shared helpers ────────────────────────────────────────────────────────────
/** Render a kv field value: muted span + caption tooltip for placeholders. */
function FieldValue({ field }: { field: PticaField }) {
if (field.isReal) return <>{field.value}</>;
return (
<span className={styles.kvPlaceholder} title={field.caption}>
{field.value}
</span>
);
}
function KvRows({ rows }: { rows: DrawerKvRow[] }) {
return (
<>
{rows.map((r) => (
<DKvRow
key={r.k}
k={r.k}
v={<FieldValue field={r.field} />}
muted={!r.field.isReal}
/>
))}
</>
);
}
function factorBars(factors: PticaFactorBar[]) {
return factors.map((f) => ({
name: f.name,
pct: f.pct,
value: f.value,
tone: f.tone,
}));
}
// ══════════════════════════════════════════════════════════════════════════════
// passport
// ══════════════════════════════════════════════════════════════════════════════
export function PassportContent({ analysis }: { analysis: ParcelAnalysis }) {
const d = adaptPassportDrawer(analysis);
return (
<>
<DrawerSection>
<SummaryBox>{d.summary}</SummaryBox>
</DrawerSection>
<DrawerSection heading="ХАРАКТЕРИСТИКИ УЧАСТКА">
<KvRows rows={d.registry} />
</DrawerSection>
<DrawerSection heading="ОГРАНИЧЕНИЯ · ЗОУИТ">
<StatusRow
label="Зоны с особыми условиями (ЗОУИТ)"
state={d.zouitCount != null ? `${d.zouitCount}` : "нет данных НСПД"}
tone={d.zouitCount != null && d.zouitCount > 0 ? "warn" : "ok"}
/>
{d.zouitOverlaps.length > 0 && (
<DTable
columns={[{ header: "Слой" }, { header: "Подкатегория" }]}
rows={d.zouitOverlaps.map((z) => ({ cells: [z.name, z.sub] }))}
/>
)}
</DrawerSection>
<SourceLine
source="Источник: ЕГРН (Росреестр) · НСПД"
updated={SRC_UPDATED}
/>
<DrawerSection>
<DrawerActions labels={["Экспорт CSV", "Копировать"]} />
</DrawerSection>
</>
);
}
// ══════════════════════════════════════════════════════════════════════════════
// buildability
// ══════════════════════════════════════════════════════════════════════════════
export function BuildabilityContent({
analysis,
}: {
analysis: ParcelAnalysis;
}) {
const d = adaptBuildabilityDrawer(analysis);
return (
<>
<DrawerSection>
<div className={styles.drawerGaugeRow}>
<BuildabilityGauge gauge={d.gauge} title="Buildability Index" />
</div>
<SummaryBox>{d.summary}</SummaryBox>
</DrawerSection>
<DrawerSection heading="ВКЛАД ФАКТОРОВ · предв.">
<HBars items={factorBars(d.factors)} />
<DTable
columns={[
{ header: "Фактор" },
{ header: "Оценка" },
{ header: "Обоснование" },
]}
rows={d.factors.map((f) => ({ cells: [f.name, f.value, f.reason] }))}
/>
</DrawerSection>
<DrawerSection heading="ГЕОМЕТРИЯ УЧАСТКА">
<KvRows rows={d.geometry} />
{d.penalties.length > 0 && (
<DTable
columns={[{ header: "Штраф" }, { header: "Влияние" }]}
rows={d.penalties.map((p) => ({ cells: [p.label, p.impact] }))}
/>
)}
</DrawerSection>
<SourceLine
source="Источник: gate_verdict (НСПД) · geometry_suitability · utilities"
updated={SRC_UPDATED}
/>
</>
);
}
// ══════════════════════════════════════════════════════════════════════════════
// urban
// ══════════════════════════════════════════════════════════════════════════════
export function UrbanContent({ analysis }: { analysis: ParcelAnalysis }) {
const d = adaptUrbanDrawer(analysis);
return (
<>
<DrawerSection>
<SummaryBox>{d.summary}</SummaryBox>
</DrawerSection>
<DrawerSection heading="РЕГЛАМЕНТ ЗОНЫ">
<KvRows rows={d.regulation} />
</DrawerSection>
<DrawerSection heading="НСПД · ЗОНИРОВАНИЕ">
<StatusRow
label="Территориальная зона"
state={<FieldValue field={d.zoneCode} />}
tone={d.zoneCode.isReal ? "ok" : "muted"}
/>
<StatusRow
label="Наименование зоны"
state={<FieldValue field={d.zoneName} />}
tone={d.zoneName.isReal ? "ok" : "muted"}
/>
</DrawerSection>
<SourceLine
source="Источник: НСПД-зонирование (nspd_zoning) · ЕГРН"
updated={SRC_UPDATED}
/>
</>
);
}
// ══════════════════════════════════════════════════════════════════════════════
// restrictions
// ══════════════════════════════════════════════════════════════════════════════
export function RestrictionsContent({
analysis,
}: {
analysis: ParcelAnalysis;
}) {
const d = adaptRestrictionsDrawer(analysis);
return (
<>
<DrawerSection>
<SummaryBox>{d.summary}</SummaryBox>
</DrawerSection>
<DrawerSection heading="СТАТУС ОГРАНИЧЕНИЙ">
{d.statusRows.map((r) => (
<StatusRow
key={r.label}
label={r.label}
state={r.state}
tone={r.tone}
/>
))}
</DrawerSection>
{d.zouitRows.length > 0 && (
<DrawerSection heading="ПЕРЕСЕЧЕНИЯ ЗОУИТ">
<DTable
columns={[
{ header: "Слой" },
{ header: "Категория" },
{ header: "Группа" },
]}
rows={d.zouitRows.map((z) => ({ cells: [z.layer, z.sub, z.reg] }))}
/>
</DrawerSection>
)}
<SourceLine
source="Источник: НСПД (nspd_zouit_overlaps) · ЕГРН"
updated={SRC_UPDATED}
/>
</>
);
}
// ══════════════════════════════════════════════════════════════════════════════
// legal
// ══════════════════════════════════════════════════════════════════════════════
export function LegalContent({ analysis }: { analysis: ParcelAnalysis }) {
const d = adaptLegalDrawer(analysis);
return (
<>
<DrawerSection>
<SummaryBox>{d.summary}</SummaryBox>
</DrawerSection>
<DrawerSection heading="ГЕЙТ-ВЕРДИКТ">
<KvRows rows={d.gate} />
</DrawerSection>
<DrawerSection heading="ОБРЕМЕНЕНИЯ И ОГРАНИЧЕНИЯ">
{d.restrictions.map((r) => (
<StatusRow
key={r.label}
label={r.label}
state={r.state}
tone={r.tone}
/>
))}
<KvRows rows={d.registry} />
</DrawerSection>
<SourceLine
source="Источник: gate_verdict · ЕГРН · НСПД"
updated={SRC_UPDATED}
/>
</>
);
}
// ══════════════════════════════════════════════════════════════════════════════
// engineering
// ══════════════════════════════════════════════════════════════════════════════
export function EngineeringContent({ analysis }: { analysis: ParcelAnalysis }) {
const d = adaptEngineeringDrawer(analysis);
return (
<>
<DrawerSection>
<SummaryBox>{d.summary}</SummaryBox>
</DrawerSection>
{d.hasData ? (
<>
<DrawerSection heading="БЛИЖАЙШИЕ ПОДКЛЮЧЕНИЯ (НСПД)">
<DTable
columns={[
{ header: "Ресурс" },
{ header: "Дистанция", num: true },
{ header: "Объект" },
{ header: "В радиусе 2 км", num: true },
]}
rows={d.rows.map((r) => ({
cells: [r.label, r.nearest, r.name, r.count],
}))}
/>
</DrawerSection>
<DrawerSection heading="УДАЛЁННОСТЬ ОТ УЗЛОВ">
<HBars items={factorBars(d.bars)} />
</DrawerSection>
</>
) : (
<DrawerSection>
<div className={styles.emptyState}>
Нет данных по инженерным сетям
</div>
</DrawerSection>
)}
<SourceLine
source="Источник: utilities.summary (НСПД)"
updated={SRC_UPDATED}
/>
{d.hasData && (
<DrawerSection>
<DrawerActions labels={["Экспорт CSV", "Копировать"]} />
</DrawerSection>
)}
</>
);
}
// ══════════════════════════════════════════════════════════════════════════════
// market (4 sub-tabs)
// ══════════════════════════════════════════════════════════════════════════════
export function MarketContent({ analysis }: { analysis: ParcelAnalysis }) {
const d = adaptMarketDrawer(analysis);
const compPanel = (
<>
{d.competitors.length > 0 ? (
<DTable
columns={[
{ header: "ЖК" },
{ header: "Застройщик" },
{ header: "Класс" },
{ header: "Квартир", num: true },
{ header: "Дист.", num: true },
{ header: "Статус" },
]}
rows={d.competitors.map((c) => ({
cells: [c.name, c.dev, c.cls, c.flats, c.distance, c.status],
}))}
/>
) : (
<div className={styles.emptyState}>Конкуренты не найдены</div>
)}
<DKvRow
k="Медиана цены района"
v={<FieldValue field={d.medianField} />}
muted={!d.medianField.isReal}
/>
</>
);
const planPanel = (
<>
<SummaryBox>{d.plan.note}</SummaryBox>
{d.plan.available && d.plan.pipelineByClass.length > 0 && (
<DTable
columns={[{ header: "Класс" }, { header: "Проектов", num: true }]}
rows={d.plan.pipelineByClass.map((p) => ({
cells: [p.cls, String(p.count)],
}))}
/>
)}
</>
);
const speedPanel = d.speed.available ? (
<>
<DKvRow k="Velocity score" v={d.speed.velocityScore} />
<DKvRow k="Темп (объект)" v={d.speed.monthly} />
<DKvRow k="Медиана ЕКБ" v={d.speed.median} />
<DKvRow k="Уверенность" v={d.speed.confidence} />
</>
) : (
<div className={styles.emptyState}>Скорость после прогноза</div>
);
const trendPanel = d.trend.available ? (
<>
<DKvRow k="Δ цены за 6 мес" v={<FieldValue field={d.trend.field} />} />
<DKvRow k="Текущая средняя" v={d.trend.recent} />
<DKvRow k="Прошлая средняя" v={d.trend.prior} />
<DKvRow k="Сделок (период)" v={d.trend.count} />
</>
) : (
<div className={styles.emptyState}>Тренд после прогноза</div>
);
return (
<>
<DrawerSection>
<SummaryBox>{d.summary}</SummaryBox>
</DrawerSection>
<DrawerSection heading="СРАВНЕНИЕ И ДИНАМИКА">
<DrawerTabs
tabs={[
{ id: "comp", label: "Конкуренты", content: compPanel },
{ id: "plan", label: "Планировки", content: planPanel },
{ id: "speed", label: "Скорость", content: speedPanel },
{ id: "trend", label: "Тренд", content: trendPanel },
]}
/>
</DrawerSection>
<SourceLine
source="Источник: competitors · market_trend · velocity · pipeline_24mo"
updated={SRC_UPDATED}
/>
{d.competitors.length > 0 && (
<DrawerSection>
<DrawerActions labels={["Экспорт CSV", "Копировать"]} />
</DrawerSection>
)}
</>
);
}
// ══════════════════════════════════════════════════════════════════════════════
// environment (Среда — 4 sub-tabs)
// ══════════════════════════════════════════════════════════════════════════════
export function EnvironmentContent({ analysis }: { analysis: ParcelAnalysis }) {
const d = adaptEnvironmentDrawer(analysis);
const noisePanel = (
<>
<KvRows rows={d.noise.rows} />
{d.noise.sources.length > 0 && (
<DTable
columns={[
{ header: "Источник" },
{ header: "дБ", num: true },
{ header: "Дист.", num: true },
]}
rows={d.noise.sources.map((s) => ({ cells: [s.name, s.db, s.dist] }))}
/>
)}
</>
);
return (
<>
<DrawerSection>
<SummaryBox>{d.summary}</SummaryBox>
</DrawerSection>
<DrawerSection heading="ЭКОЛОГИЯ И КЛИМАТ">
<DrawerTabs
tabs={[
{ id: "noise", label: "Шум", content: noisePanel },
{
id: "air",
label: "Воздух",
content: <KvRows rows={d.air.rows} />,
},
{
id: "wind",
label: "Ветер",
content: <KvRows rows={d.wind.rows} />,
},
{
id: "weather",
label: "Погода",
content: <KvRows rows={d.weather.rows} />,
},
]}
/>
</DrawerSection>
<SourceLine
source="Источник: noise · air_quality · wind · weather (/analyze)"
updated={SRC_UPDATED}
/>
</>
);
}
// ══════════════════════════════════════════════════════════════════════════════
// georisks (3 sub-tabs)
// ══════════════════════════════════════════════════════════════════════════════
export function GeorisksContent({ analysis }: { analysis: ParcelAnalysis }) {
const d = adaptGeorisksDrawer(analysis);
const geologyPanel = (
<>
<KvRows rows={d.geology.rows} />
{d.geology.links.length > 0 && (
<div className={styles.drawerLinks}>
{d.geology.links.map((l) => (
<a
key={l.href}
href={l.href}
target="_blank"
rel="noopener noreferrer"
className={styles.drawerLink}
>
{l.label}
</a>
))}
</div>
)}
</>
);
return (
<>
<DrawerSection>
<SummaryBox>{d.summary}</SummaryBox>
</DrawerSection>
<DrawerSection heading="ИНЖЕНЕРНО-ГЕОЛОГИЧЕСКИЕ УСЛОВИЯ">
<DrawerTabs
tabs={[
{ id: "geology", label: "Геология", content: geologyPanel },
{
id: "hydro",
label: "Гидрология",
content: <KvRows rows={d.hydrology.rows} />,
},
{
id: "geotech",
label: "Геотехника",
content: <KvRows rows={d.geotech.rows} />,
},
]}
/>
</DrawerSection>
<SourceLine
source="Источник: geology · hydrology · geotech_risk (/analyze)"
updated={SRC_UPDATED}
/>
</>
);
}
// ══════════════════════════════════════════════════════════════════════════════
// risks (composite, derived)
// ══════════════════════════════════════════════════════════════════════════════
export function RisksContent({ analysis }: { analysis: ParcelAnalysis }) {
const d = adaptRisksDrawer(analysis);
return (
<>
<DrawerSection>
<div className={styles.drawerGaugeRow}>
<BuildabilityGauge gauge={d.gauge} title="Сводный риск" />
</div>
<SummaryBox>{d.summary}</SummaryBox>
</DrawerSection>
<DrawerSection heading="РАЗЛОЖЕНИЕ РИСКА · предв.">
<HBars items={factorBars(d.breakdown)} />
<DTable
columns={[
{ header: "Компонент" },
{ header: "Оценка" },
{ header: "Источник" },
]}
rows={d.breakdown.map((f) => ({
cells: [f.name, f.value, f.reason],
}))}
/>
</DrawerSection>
{(d.blockers.length > 0 || d.warnings.length > 0) && (
<DrawerSection heading="ГЕЙТ · БЛОКЕРЫ И ПРЕДУПРЕЖДЕНИЯ">
{d.blockers.map((b) => (
<StatusRow
key={b.code}
label={b.detail}
state="блокер"
tone="bad"
/>
))}
{d.warnings.map((w) => (
<StatusRow
key={w.code}
label={w.detail}
state="предупр."
tone="warn"
/>
))}
</DrawerSection>
)}
<SourceLine
source="Источник: gate_verdict · geometry_suitability · noise"
updated={SRC_UPDATED}
/>
</>
);
}
// ══════════════════════════════════════════════════════════════════════════════
// product (§22 product_tz)
// ══════════════════════════════════════════════════════════════════════════════
export function ProductContent({
forecastReport,
}: {
forecastReport?: ForecastReport;
}) {
const d = adaptProductDrawer(forecastReport);
if (!d.available) {
return (
<>
<DrawerSection>
<SummaryBox>{d.summary}</SummaryBox>
</DrawerSection>
<DrawerSection>
<div className={styles.emptyState}>После прогноза (Сценарии)</div>
</DrawerSection>
</>
);
}
return (
<>
<DrawerSection>
<SummaryBox>{d.summary}</SummaryBox>
</DrawerSection>
<DrawerSection heading="РЕКОМЕНДОВАННЫЙ КЛАСС">
<DKvRow
k="Класс продукта"
v={
d.objClass.isReal ? (
<ConfPill level="high">{d.objClass.value}</ConfPill>
) : (
<FieldValue field={d.objClass} />
)
}
muted={!d.objClass.isReal}
/>
</DrawerSection>
{d.mix.length > 0 && (
<DrawerSection heading="КВАРТИРОГРАФИЯ">
<HBars
items={d.mix.map((m) => ({
name: m.bucket,
pct: m.pct,
value: m.signal,
tone: "neutral",
}))}
/>
</DrawerSection>
)}
{d.usp.length > 0 && (
<DrawerSection heading="USP-НИШИ">
<DTable
columns={[{ header: "Ниша" }, { header: "Класс" }]}
rows={d.usp.map((u) => ({ cells: [u.text, u.cls] }))}
/>
</DrawerSection>
)}
{d.reasons.length > 0 && (
<DrawerSection heading="ОБОСНОВАНИЕ">
{d.reasons.map((r, i) => (
<p key={i} className={styles.drawerNote}>
{r}
</p>
))}
</DrawerSection>
)}
<SourceLine
source="Источник: §22 product_tz (advisory)"
updated={SRC_UPDATED}
/>
</>
);
}
// ══════════════════════════════════════════════════════════════════════════════
// potential
// ══════════════════════════════════════════════════════════════════════════════
export function PotentialContent({ analysis }: { analysis: ParcelAnalysis }) {
const d = adaptPotentialDrawer(analysis);
return (
<>
<DrawerSection>
<SummaryBox>{d.summary}</SummaryBox>
</DrawerSection>
<DrawerSection heading="ЦЕПОЧКА РАСЧЁТА ЁМКОСТИ">
<KvRows rows={d.rows} />
</DrawerSection>
<SourceLine
source="Источник: geometry_suitability · НСПД-зонирование (max_far)"
updated={SRC_UPDATED}
/>
</>
);
}
// ══════════════════════════════════════════════════════════════════════════════
// economy / finance
// ══════════════════════════════════════════════════════════════════════════════
export function EconomyContent() {
const d = adaptEconomyDrawer();
return (
<>
<DrawerSection>
<SummaryBox>{d.summary}</SummaryBox>
</DrawerSection>
<DrawerSection heading="КЛЮЧЕВЫЕ ПОКАЗАТЕЛИ · после финмодели">
<KvRows rows={d.rows} />
</DrawerSection>
<SourceLine source="Источник: финмодель §22 (асинхронно)" />
</>
);
}
export function FinanceContent({ analysis }: { analysis: ParcelAnalysis }) {
const financial = analysis.financial_estimate;
const d = adaptFinanceDrawer(financial);
const hasData = financial != null;
return (
<>
<DrawerSection>
<SummaryBox>{d.summary}</SummaryBox>
</DrawerSection>
<DrawerSection
heading={
hasData
? "ФИНАНСОВАЯ МОДЕЛЬ · по макс. застройке НСПД"
: "ФИНАНСОВАЯ МОДЕЛЬ · после финмодели"
}
>
<KvRows rows={d.rows} />
</DrawerSection>
<SourceLine
source={
hasData
? "Источник: финмодель /analyze (DCF · по макс. застройке НСПД)"
: "Источник: финмодель §22 (DCF, асинхронно)"
}
/>
</>
);
}
// ══════════════════════════════════════════════════════════════════════════════
// oks
// ══════════════════════════════════════════════════════════════════════════════
export function OksContent() {
const d = adaptOksDrawer();
return (
<>
<DrawerSection>
<SummaryBox>{d.summary}</SummaryBox>
</DrawerSection>
<DrawerSection>
<div className={styles.emptyState}>Нет данных</div>
</DrawerSection>
</>
);
}
// ══════════════════════════════════════════════════════════════════════════════
// insolation / future3d — interactive 3D massing sandbox (Three.js)
// ══════════════════════════════════════════════════════════════════════════════
export function Future3dContent({ analysis }: { analysis: ParcelAnalysis }) {
const maxFar = massingMaxFar(analysis);
const farReal = massingFarIsReal(analysis);
return (
<>
<DrawerSection>
<SummaryBox>
Генеративная масса по регламенту: КСИТ-цель {maxFar.toFixed(1)} ·
пятно и высота подбираются под целевую ёмкость, GFA и КСИТ-факт
считаются от вводных. Орбита, зум и солнце интерактивные.
</SummaryBox>
</DrawerSection>
<DrawerSection>
<MassingViewer
areaM2={massingAreaM2(analysis)}
maxFar={maxFar}
farIsReal={farReal}
/>
</DrawerSection>
<SourceLine
source={
farReal
? "Песочница массинга · КСИТ-цель регламент НСПД (max_far)"
: "Песочница массинга · КСИТ-цель регламент-дефолт (НСПД)"
}
/>
</>
);
}
export function InsolationContent({ analysis }: { analysis: ParcelAnalysis }) {
return (
<>
<DrawerSection>
<SummaryBox>
Предпросмотр инсоляции на 3D-массе: солнце-источник теней управляется
ползунком «Время суток» (06:0020:00). Полный расчёт КЕО и
продолжительности инсоляции по СанПиН в следующей версии.
</SummaryBox>
</DrawerSection>
<DrawerSection>
<MassingViewer
areaM2={massingAreaM2(analysis)}
maxFar={massingMaxFar(analysis)}
farIsReal={massingFarIsReal(analysis)}
/>
</DrawerSection>
<DrawerSection heading="ЧТО ДОБАВИТСЯ В МОДУЛЕ">
<StatusRow
label="Карта инсоляции фасадов"
state="в разработке"
tone="warn"
/>
<StatusRow
label="Теневой анализ по сезонам"
state="в разработке"
tone="warn"
/>
<StatusRow
label="Проверка норм СанПиН / КЕО"
state="в разработке"
tone="warn"
/>
</DrawerSection>
<SourceLine source="Тень от солнца = базовый предпросмотр инсоляции" />
</>
);
}