This commit is contained in:
parent
d9915c7834
commit
f448b550b5
2 changed files with 102 additions and 21 deletions
|
|
@ -114,16 +114,19 @@ function KpiGrid({
|
|||
label="Общая площадь (GFA)"
|
||||
value={formatInt(teap.total_floor_area_sqm)}
|
||||
unit="м²"
|
||||
hint="Поэтажная площадь всех корпусов = пятно застройки × этажность."
|
||||
/>
|
||||
<KpiCard
|
||||
label="Продаваемая площадь"
|
||||
value={formatInt(teap.residential_area_sqm)}
|
||||
unit="м²"
|
||||
hint="Жилая к продаже = (GFA − нежилой 1-й этаж) × коэффициент эффективности класса."
|
||||
/>
|
||||
<KpiCard
|
||||
label="Квартир"
|
||||
value={formatInt(teap.apartments_count)}
|
||||
unit="шт"
|
||||
hint="Продаваемая площадь ÷ средняя площадь квартиры класса."
|
||||
/>
|
||||
<KpiCard
|
||||
label="КСИТ — факт / цель"
|
||||
|
|
@ -134,6 +137,7 @@ function KpiGrid({
|
|||
: "В пределах регламента",
|
||||
positive: ksitOver ? false : true,
|
||||
}}
|
||||
hint="КСИТ (коэффициент строительного использования) = надземная GFA ÷ площадь участка. Цель — предельный max_far по регламенту НСПД."
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -149,10 +153,12 @@ function KpiGrid({
|
|||
<KpiCard
|
||||
label="Выручка (GDV)"
|
||||
value={formatMoneyCompact(financial.revenue_rub)}
|
||||
hint="Продаваемая площадь × цена продажи м² (+ машиноместа и нежилой 1-й этаж по ценам класса)."
|
||||
/>
|
||||
<KpiCard
|
||||
label="Себестоимость"
|
||||
value={formatMoneyCompact(financial.cost_rub)}
|
||||
hint="Строительство (GFA × удельная по классу) + ПИР, сети, услуги застройщика, непредвиденные, маркетинг + стоимость земли."
|
||||
/>
|
||||
<KpiCard
|
||||
label="Чистая прибыль"
|
||||
|
|
@ -166,6 +172,7 @@ function KpiGrid({
|
|||
: "Нулевая",
|
||||
positive: netPositive,
|
||||
}}
|
||||
hint="Выручка − себестоимость − НДС на нежилое − налог на прибыль 25%. Жильё по ДДУ от НДС освобождено."
|
||||
/>
|
||||
<KpiCard
|
||||
label="ROI на затраты"
|
||||
|
|
@ -174,6 +181,7 @@ function KpiGrid({
|
|||
value: `Чистая маржа на выручку ${formatPct(financial.margin_pct)}`,
|
||||
positive: null,
|
||||
}}
|
||||
hint="Чистая прибыль ÷ себестоимость. Маржа считается от выручки."
|
||||
/>
|
||||
<KpiCard
|
||||
label="NPV (DCF)"
|
||||
|
|
@ -187,6 +195,9 @@ function KpiGrid({
|
|||
? false
|
||||
: null,
|
||||
}}
|
||||
hint={`Сумма дисконтированных помесячных денежных потоков по графику стройки и продаж (ставка дисконта ${formatPct(
|
||||
financial.discount_rate_used,
|
||||
)} годовых).`}
|
||||
/>
|
||||
<KpiCard
|
||||
label="IRR (DCF, годовой)"
|
||||
|
|
@ -201,11 +212,13 @@ function KpiGrid({
|
|||
? true
|
||||
: false,
|
||||
}}
|
||||
hint="Годовая внутренняя ставка доходности тех же денежных потоков (ставка, при которой NPV = 0)."
|
||||
/>
|
||||
<KpiCard
|
||||
label="Цена продажи жилья"
|
||||
value={`${formatInt(financial.price_per_sqm_used)} ₽/м²`}
|
||||
delta={{ value: priceSourceCaption(financial), positive: null }}
|
||||
hint="Цена, заложенная в выручку: медиана объявлений Objective по району (источник указан выше)."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -253,9 +266,20 @@ interface Props {
|
|||
farTarget: number;
|
||||
/** True when факт-КСИТ exceeds the cap (model.over, computed scene-side). */
|
||||
ksitOver: boolean;
|
||||
/**
|
||||
* True when the parcel is regulatorily constrained for МКД (gate-blocked /
|
||||
* non-residential / ЗОУИТ-СЗЗ) — drives the honest «условный расчёт» caveat
|
||||
* below the strip alongside the negative-economics case.
|
||||
*/
|
||||
gateConstrained?: boolean;
|
||||
}
|
||||
|
||||
export function MassingEconomics({ program, farTarget, ksitOver }: Props) {
|
||||
export function MassingEconomics({
|
||||
program,
|
||||
farTarget,
|
||||
ksitOver,
|
||||
gateConstrained = false,
|
||||
}: Props) {
|
||||
const recompute = useRecomputeMassing();
|
||||
|
||||
// Last successful result kept locally so a failed/stale recompute never blanks
|
||||
|
|
@ -316,6 +340,13 @@ export function MassingEconomics({ program, farTarget, ksitOver }: Props) {
|
|||
// A fresher request is in flight over the last-good values.
|
||||
const stale = recompute.isPending;
|
||||
|
||||
// Honest «условный расчёт» caveat: when the economics turn negative OR the
|
||||
// parcel is regulatorily constrained (gate-blocked / ЗОУИТ-СЗЗ / нежилое),
|
||||
// we say so plainly rather than presenting the figures as a viable project.
|
||||
const economicsNegative =
|
||||
result.financial.net_profit_rub < 0 || result.financial.npv_rub < 0;
|
||||
const showConditionalNote = economicsNegative || gateConstrained;
|
||||
|
||||
return (
|
||||
<Section
|
||||
title="Экономика по 3D-модели"
|
||||
|
|
@ -329,6 +360,33 @@ export function MassingEconomics({ program, farTarget, ksitOver }: Props) {
|
|||
stale={stale}
|
||||
/>
|
||||
|
||||
{showConditionalNote ? (
|
||||
<p
|
||||
role="note"
|
||||
style={{
|
||||
margin: "12px 0 0",
|
||||
display: "flex",
|
||||
alignItems: "flex-start",
|
||||
gap: 8,
|
||||
fontSize: 12,
|
||||
lineHeight: "16px",
|
||||
color: "var(--warn)",
|
||||
}}
|
||||
>
|
||||
<AlertTriangle
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
aria-hidden="true"
|
||||
style={{ flexShrink: 0, marginTop: 1 }}
|
||||
/>
|
||||
<span>
|
||||
Расчёт условный: участок ограничен регламентом (см. блокеры выше)
|
||||
и/или экономика отрицательна при текущих вводных. Измените
|
||||
этажность, число секций или класс — модель пересчитается.
|
||||
</span>
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{errored ? (
|
||||
<p
|
||||
role="status"
|
||||
|
|
|
|||
|
|
@ -1,13 +1,22 @@
|
|||
"use client";
|
||||
|
||||
/**
|
||||
* Section7Concept — «7. Концепция» (epic #1953, #1965 Stage 1).
|
||||
* Section7Concept — «7. Концепция» (epic #1953, #1965 Stage 2b).
|
||||
*
|
||||
* Folds the generative concept generator (3 strategy variants: placement map +
|
||||
* ТЭП + financial model) into the LIGHT analysis report as a new section AFTER
|
||||
* «6. Прогноз». Frontend-only: it reuses the existing concept components
|
||||
* (ConceptParamsForm / ConceptVariantsResult) and the /concepts mutation
|
||||
* (useCreateConcept) AS-IS — no duplication, no backend change.
|
||||
* Folds the generative concept tooling into the LIGHT analysis report as a new
|
||||
* section AFTER «6. Прогноз». Frontend-only — reuses the existing concept
|
||||
* components and the /concepts mutations AS-IS (no duplication, no backend
|
||||
* change). Two distinct surfaces sit in this section:
|
||||
*
|
||||
* 1. LIVE interactive 3D massing (top): MassingViewer + MassingEconomics.
|
||||
* Dragging the этажность / секции sliders re-folds the program and the
|
||||
* economics strip recomputes ТЭП + финмодель vживую via /concepts/recompute
|
||||
* (debounced). This is the primary, always-on surface.
|
||||
* 2. ON-DEMAND variant layouts (bottom): ConceptParamsForm + button →
|
||||
* useCreateConcept → /concepts returns up to 3 placement strategies
|
||||
* (max_area / max_insolation / balanced) rendered on a Leaflet map. Heavy,
|
||||
* so it runs only from the button, never auto on mount, and the result map
|
||||
* is lazy-mounted via `dynamic(…, { ssr: false })`.
|
||||
*
|
||||
* Inputs are seeded from the analysis the report already has (no re-entry of the
|
||||
* cadastre / polygon):
|
||||
|
|
@ -17,16 +26,12 @@
|
|||
* • land_cost_rub ← egrn.cadastral_value_rub
|
||||
* Falls back to comfort / mid_rise when those are null.
|
||||
*
|
||||
* On-demand only: the generative call is heavy, so it runs from a button — never
|
||||
* auto on mount. The result block (Leaflet map) is lazy-mounted via `dynamic(…,
|
||||
* { ssr: false })`. The section defaults to collapsed (above-the-fold rule).
|
||||
* The section defaults to collapsed (above-the-fold rule).
|
||||
*
|
||||
* СЗЗ / financial_estimate === null is NOT a dead end: /concepts only needs a
|
||||
* polygon + the form inputs (it is independent of the financial_estimate
|
||||
* regulatory gate), so we show an honest banner and STILL allow a manual run.
|
||||
*
|
||||
* Stage 2 (separate PR) wires an interactive 3D MassingScene + a /concepts/
|
||||
* recompute endpoint for live economics — left as a clear seam here, not built.
|
||||
* СЗЗ / financial_estimate === null is NOT a dead end: the massing + /concepts
|
||||
* only need a polygon + form inputs (independent of the financial_estimate
|
||||
* regulatory gate), so we show an honest banner, mark the live economics as
|
||||
* «условный расчёт», and STILL allow a manual run.
|
||||
*/
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
|
|
@ -198,6 +203,12 @@ export function Section7Concept({ analysis }: Props) {
|
|||
|
||||
const financeUnavailable = fin == null;
|
||||
|
||||
// Regulatorily constrained for МКД: no регламентная финмодель (ЗОУИТ / СЗЗ /
|
||||
// нет зоны → financial_estimate == null) OR the gate verdict says МКД нельзя.
|
||||
// Drives the honest «условный расчёт» caveat in the live economics strip.
|
||||
const gateConstrained =
|
||||
financeUnavailable || analysis.gate_verdict?.can_build_mkd === false;
|
||||
|
||||
const parcelAreaSqm = polygon ? polygonAreaSqm(polygon) : 0;
|
||||
|
||||
// КСИТ-цель (max_far) fed to the 3D scene + economics: real ПЗЗ-регламент when
|
||||
|
|
@ -280,7 +291,7 @@ export function Section7Concept({ analysis }: Props) {
|
|||
<section id="section-7" style={{ scrollMarginTop: SCROLL_MARGIN }}>
|
||||
<HeadlineBar
|
||||
title="7. Концепция"
|
||||
subtitle="Генеративный дизайн застройки: три стратегии размещения с расчётом ТЭП и финансовой модели. Считается по запросу из геометрии и параметров этого участка."
|
||||
subtitle="Интерактивная 3D-масса застройки: меняете класс, тип, этажность и число секций — ТЭП и финмодель пересчитываются вживую по геометрии этого участка. Отдельной кнопкой ниже движок раскладывает варианты размещения."
|
||||
/>
|
||||
|
||||
{/* Collapsed by default (above-the-fold rule) — form + button inside;
|
||||
|
|
@ -358,7 +369,7 @@ export function Section7Concept({ analysis }: Props) {
|
|||
>
|
||||
<Section
|
||||
title="Интерактивная масса застройки"
|
||||
subtitle={`Меняйте этажность и число секций — экономика справа пересчитывается по финмодели. КСИТ-цель ${farTarget.toFixed(
|
||||
subtitle={`Редактируемая 3D-модель: тяните ползунки этажности и числа секций — ТЭП и финмодель справа пересчитываются вживую. КСИТ-цель ${farTarget.toFixed(
|
||||
1,
|
||||
)} (${
|
||||
farIsReal ? "регламент НСПД" : "дефолт, источник НСПД"
|
||||
|
|
@ -375,6 +386,7 @@ export function Section7Concept({ analysis }: Props) {
|
|||
program={program}
|
||||
farTarget={farTarget}
|
||||
ksitOver={ksitOver}
|
||||
gateConstrained={gateConstrained}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -397,9 +409,20 @@ export function Section7Concept({ analysis }: Props) {
|
|||
color: "var(--fg-primary)",
|
||||
}}
|
||||
>
|
||||
Кадастровый номер {analysis.cad_num} · площадь ≈{" "}
|
||||
{ha(parcelAreaSqm)} га.
|
||||
Кадастровый номер {analysis.cad_num} · площадь пятна по
|
||||
геометрии ≈ {ha(parcelAreaSqm)} га.
|
||||
</div>
|
||||
<p
|
||||
style={{
|
||||
margin: "8px 0 0",
|
||||
fontSize: 12,
|
||||
color: "var(--fg-tertiary)",
|
||||
}}
|
||||
>
|
||||
Массинг и КСИТ считаются от площади застраиваемого пятна по
|
||||
геометрии участка (контур НСПД), поэтому она может
|
||||
отличаться от площади по ЕГРН в карточке участка.
|
||||
</p>
|
||||
<p
|
||||
style={{
|
||||
margin: "8px 0 0",
|
||||
|
|
@ -415,7 +438,7 @@ export function Section7Concept({ analysis }: Props) {
|
|||
|
||||
<Section
|
||||
title="Параметры проекта"
|
||||
subtitle="Класс, тип застройки и этажность. Можно задать программу из типовых домов вместо авторазмещения."
|
||||
subtitle="Задайте класс, тип застройки и этажность для расчёта вариантов размещения по кнопке ниже (отдельно от 3D-массы сверху). Можно указать программу из типовых домов вместо авторазмещения."
|
||||
>
|
||||
<div style={{ display: "grid", gap: 16 }}>
|
||||
<HouseProgramPicker
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue