"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 ( {field.value} ); } function KvRows({ rows }: { rows: DrawerKvRow[] }) { return ( <> {rows.map((r) => ( } 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 ( <> {d.summary} 0 ? "warn" : "ok"} /> {d.zouitOverlaps.length > 0 && ( ({ cells: [z.name, z.sub] }))} /> )} ); } // ══════════════════════════════════════════════════════════════════════════════ // buildability // ══════════════════════════════════════════════════════════════════════════════ export function BuildabilityContent({ analysis, }: { analysis: ParcelAnalysis; }) { const d = adaptBuildabilityDrawer(analysis); return ( <>
{d.summary}
({ cells: [f.name, f.value, f.reason] }))} /> {d.penalties.length > 0 && ( ({ cells: [p.label, p.impact] }))} /> )} ); } // ══════════════════════════════════════════════════════════════════════════════ // urban // ══════════════════════════════════════════════════════════════════════════════ export function UrbanContent({ analysis }: { analysis: ParcelAnalysis }) { const d = adaptUrbanDrawer(analysis); return ( <> {d.summary} } tone={d.zoneCode.isReal ? "ok" : "muted"} /> } tone={d.zoneName.isReal ? "ok" : "muted"} /> ); } // ══════════════════════════════════════════════════════════════════════════════ // restrictions // ══════════════════════════════════════════════════════════════════════════════ export function RestrictionsContent({ analysis, }: { analysis: ParcelAnalysis; }) { const d = adaptRestrictionsDrawer(analysis); return ( <> {d.summary} {d.statusRows.map((r) => ( ))} {d.zouitRows.length > 0 && ( ({ cells: [z.layer, z.sub, z.reg] }))} /> )} ); } // ══════════════════════════════════════════════════════════════════════════════ // legal // ══════════════════════════════════════════════════════════════════════════════ export function LegalContent({ analysis }: { analysis: ParcelAnalysis }) { const d = adaptLegalDrawer(analysis); return ( <> {d.summary} {d.restrictions.map((r) => ( ))} ); } // ══════════════════════════════════════════════════════════════════════════════ // engineering // ══════════════════════════════════════════════════════════════════════════════ export function EngineeringContent({ analysis }: { analysis: ParcelAnalysis }) { const d = adaptEngineeringDrawer(analysis); return ( <> {d.summary} {d.hasData ? ( <> ({ cells: [r.label, r.nearest, r.name, r.count], }))} /> ) : (
Нет данных по инженерным сетям
)} {d.hasData && ( )} ); } // ══════════════════════════════════════════════════════════════════════════════ // market (4 sub-tabs) // ══════════════════════════════════════════════════════════════════════════════ export function MarketContent({ analysis }: { analysis: ParcelAnalysis }) { const d = adaptMarketDrawer(analysis); const compPanel = ( <> {d.competitors.length > 0 ? ( ({ cells: [c.name, c.dev, c.cls, c.flats, c.distance, c.status], }))} /> ) : (
Конкуренты не найдены
)} } muted={!d.medianField.isReal} /> ); const planPanel = ( <> {d.plan.note} {d.plan.available && d.plan.pipelineByClass.length > 0 && ( ({ cells: [p.cls, String(p.count)], }))} /> )} ); const speedPanel = d.speed.available ? ( <> ) : (
Скорость — после прогноза
); const trendPanel = d.trend.available ? ( <> } /> ) : (
Тренд — после прогноза
); return ( <> {d.summary} {d.competitors.length > 0 && ( )} ); } // ══════════════════════════════════════════════════════════════════════════════ // environment (Среда — 4 sub-tabs) // ══════════════════════════════════════════════════════════════════════════════ export function EnvironmentContent({ analysis }: { analysis: ParcelAnalysis }) { const d = adaptEnvironmentDrawer(analysis); const noisePanel = ( <> {d.noise.sources.length > 0 && ( ({ cells: [s.name, s.db, s.dist] }))} /> )} ); return ( <> {d.summary} , }, { id: "wind", label: "Ветер", content: , }, { id: "weather", label: "Погода", content: , }, ]} /> ); } // ══════════════════════════════════════════════════════════════════════════════ // georisks (3 sub-tabs) // ══════════════════════════════════════════════════════════════════════════════ export function GeorisksContent({ analysis }: { analysis: ParcelAnalysis }) { const d = adaptGeorisksDrawer(analysis); const geologyPanel = ( <> {d.geology.links.length > 0 && (
{d.geology.links.map((l) => ( {l.label} → ))}
)} ); return ( <> {d.summary} , }, { id: "geotech", label: "Геотехника", content: , }, ]} /> ); } // ══════════════════════════════════════════════════════════════════════════════ // risks (composite, derived) // ══════════════════════════════════════════════════════════════════════════════ export function RisksContent({ analysis }: { analysis: ParcelAnalysis }) { const d = adaptRisksDrawer(analysis); return ( <>
{d.summary}
({ cells: [f.name, f.value, f.reason], }))} /> {(d.blockers.length > 0 || d.warnings.length > 0) && ( {d.blockers.map((b) => ( ))} {d.warnings.map((w) => ( ))} )} ); } // ══════════════════════════════════════════════════════════════════════════════ // product (§22 product_tz) // ══════════════════════════════════════════════════════════════════════════════ export function ProductContent({ forecastReport, }: { forecastReport?: ForecastReport; }) { const d = adaptProductDrawer(forecastReport); if (!d.available) { return ( <> {d.summary}
После прогноза (Сценарии)
); } return ( <> {d.summary} {d.objClass.value} ) : ( ) } muted={!d.objClass.isReal} /> {d.mix.length > 0 && ( ({ name: m.bucket, pct: m.pct, value: m.signal, tone: "neutral", }))} /> )} {d.usp.length > 0 && ( ({ cells: [u.text, u.cls] }))} /> )} {d.reasons.length > 0 && ( {d.reasons.map((r, i) => (

{r}

))}
)} ); } // ══════════════════════════════════════════════════════════════════════════════ // potential // ══════════════════════════════════════════════════════════════════════════════ export function PotentialContent({ analysis }: { analysis: ParcelAnalysis }) { const d = adaptPotentialDrawer(analysis); return ( <> {d.summary} ); } // ══════════════════════════════════════════════════════════════════════════════ // economy / finance // ══════════════════════════════════════════════════════════════════════════════ export function EconomyContent() { const d = adaptEconomyDrawer(); return ( <> {d.summary} ); } export function FinanceContent({ analysis }: { analysis: ParcelAnalysis }) { const financial = analysis.financial_estimate; const d = adaptFinanceDrawer(financial); const hasData = financial != null; return ( <> {d.summary} ); } // ══════════════════════════════════════════════════════════════════════════════ // oks // ══════════════════════════════════════════════════════════════════════════════ export function OksContent() { const d = adaptOksDrawer(); return ( <> {d.summary}
Нет данных
); } // ══════════════════════════════════════════════════════════════════════════════ // insolation / future3d — interactive 3D massing sandbox (Three.js) // ══════════════════════════════════════════════════════════════════════════════ export function Future3dContent({ analysis }: { analysis: ParcelAnalysis }) { const maxFar = massingMaxFar(analysis); const farReal = massingFarIsReal(analysis); return ( <> Генеративная масса по регламенту: КСИТ-цель {maxFar.toFixed(1)} · пятно и высота подбираются под целевую ёмкость, GFA и КСИТ-факт считаются от вводных. Орбита, зум и солнце — интерактивные. ); } export function InsolationContent({ analysis }: { analysis: ParcelAnalysis }) { return ( <> Предпросмотр инсоляции на 3D-массе: солнце-источник теней управляется ползунком «Время суток» (06:00–20:00). Полный расчёт КЕО и продолжительности инсоляции по СанПиН — в следующей версии. ); }