"use client"; /** * ConceptVariantsResult — variant tabs + per-variant placement map, ТЭП KPIs, * financial model, and export controls. Driven by the ConceptOutput returned * from POST /concepts. The map is lazy-mounted (Leaflet uses `window`). */ import dynamic from "next/dynamic"; import { useState } from "react"; import { KpiCard } from "@/components/analytics/KpiCard"; import { Section } from "@/components/analytics/Section"; import { Badge } from "@/components/ui/Badge"; import { priceSourceCaption, STRATEGY_HINTS, STRATEGY_LABELS, type ConceptVariant, type FinancialModel, } from "@/lib/concept-api"; import type { Polygon } from "geojson"; import { ConceptExportButtons } from "./ConceptExportButtons"; const ConceptResultMap = dynamic( () => import("./ConceptResultMap").then((m) => ({ default: m.ConceptResultMap })), { ssr: false, loading: () => (
Загрузка карты…
), }, ); // ── Formatters (ru microcopy) ───────────────────────────────────────────────── const nf = new Intl.NumberFormat("ru-RU", { maximumFractionDigits: 0 }); /** Compact ₽ for headline figures: "2.4 млрд ₽", "145 млн ₽". */ function formatMoneyCompact(rub: number): string { const abs = Math.abs(rub); if (abs >= 1e9) return `${(rub / 1e9).toFixed(1)} млрд ₽`; if (abs >= 1e6) return `${(rub / 1e6).toFixed(0)} млн ₽`; return `${nf.format(Math.round(rub))} ₽`; } function formatInt(n: number): string { return nf.format(Math.round(n)); } function formatPct(fraction: number): string { return `${(fraction * 100).toFixed(1)}%`; } /** Срок окупаемости: «28.5 мес» или «не окупается» (null). */ function formatPayback(months: number | null): string { if (months === null) return "не окупается"; return `${months.toFixed(1)} мес`; } // ── Variant panel ───────────────────────────────────────────────────────────── interface PanelProps { parcel: Polygon; variant: ConceptVariant; } function VariantPanel({ parcel, variant }: PanelProps) { const { teap, financial } = variant; const netPositive = financial.net_profit_rub > 0 ? true : financial.net_profit_rub < 0 ? false : null; return (
{/* Headline-bar — one-sentence JTBD verdict for the variant. */}
{STRATEGY_LABELS[variant.strategy]}:{" "} {formatInt(teap.total_floor_area_sqm)} м² надземной площади ·{" "} {formatInt(teap.apartments_count)} квартир · чистая прибыль{" "} {formatMoneyCompact(financial.net_profit_rub)} · ROI{" "} {formatPct(financial.roi)}
{STRATEGY_HINTS[variant.strategy]} NPV{" "} {formatMoneyCompact(financial.npv_rub)} · IRR{" "} {formatPct(financial.irr)} {financial.irr_is_proxy ? " (оценочный)" : ""} · окупаемость{" "} {formatPayback(financial.payback_months)} · плотность (FAR){" "} {teap.density.toLocaleString("ru-RU", { minimumFractionDigits: 2, maximumFractionDigits: 2, })} .
{/* Placement map */}
{/* ТЭП — KPI grid */}
{/* Финмодель */}
} >
0 ? true : financial.npv_rub < 0 ? false : null, }} /> financial.discount_rate_used ? true : false, }} />
{/* Каскад затрат + БДР — под аккордеоном (плотность > выкладки) */}
Подробнее — каскад затрат и расчёт прибыли

NPV / IRR / окупаемость рассчитаны помесячным DCF.{" "} {financial.schedule_is_default ? `График фаз и темп продаж — типовые допущения (ПИР 6 мес → СМР по типу застройки → распродажа 30 мес, дисконт ${formatPct( financial.discount_rate_used, )} годовых); метрики рассчитаны DCF по этому графику, точность зависит от графика конкретного проекта. ` : ""} {financial.irr_is_proxy ? "IRR помечен как оценочный: денежный поток вырожденный (нет смены знака), показан аннуализированный ROI вместо DCF-IRR. " : ""} НДС — упрощённая оценка (встроенный НДС в положительной марже, не полная механика входного/выходного НДС НК РФ); привязан к признанию выручки. Налог на прибыль — 25% (с 2025 года). Цена продажи жилья —{" "} {financial.price_is_calibrated ? `калибрована по рынку (${priceSourceCaption(financial)})` : "норматив класса (нет рыночных данных по участку)"} ; себестоимость СМР и цена паркинга — нормативные ориентиры. Коммерческие и офисные площади не учитываются. {financial.financing_enabled && financial.financing_is_simplified ? " Финансирование упрощено: весь кассовый разрыв покрыт " + "кредитом по ставке-нормативу, проценты капитализируются, " + "эскроу не моделируется точно. Проценты — реальная стоимость " + "(чистая прибыль после финансирования ниже прибыли без него)." : ""}

); } // ── Cost cascade / P&L detail table ──────────────────────────────────────────── function CascadeRow({ label, value, emphasis = false, }: { label: string; value: string; emphasis?: boolean; }) { return ( {label} {value} ); } function FinancialCascadeTable({ financial }: { financial: FinancialModel }) { return ( {/* #1881 PR-5 — финансирование (кредит). Показываем только когда движок смоделировал финансирование (financing_enabled). */} {financial.financing_enabled ? ( <> ) : null}
); } // ── Component ───────────────────────────────────────────────────────────────── interface Props { parcel: Polygon; variants: ConceptVariant[]; } export function ConceptVariantsResult({ parcel, variants }: Props) { const [active, setActive] = useState(0); if (variants.length === 0) { return (

Движок не вернул ни одного варианта застройки.

); } const activeVariant = variants[Math.min(active, variants.length - 1)]; return (
{/* Variant tabs */}
{variants.map((v, i) => { const isActive = i === active; return ( ); })}
); }