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
905 lines
32 KiB
TypeScript
905 lines
32 KiB
TypeScript
"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:00–20: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="Тень от солнца = базовый предпросмотр инсоляции" />
|
||
</>
|
||
);
|
||
}
|