Compare commits
No commits in common. "fed33fce2747f0fb67944c50ae70eb55ea0c1dfc" and "3187419eb8a426eede84c8d221844c9cdea0def9" have entirely different histories.
fed33fce27
...
3187419eb8
2 changed files with 0 additions and 334 deletions
|
|
@ -13,7 +13,6 @@ import { Section3SettingsAndCompetitors } from "@/components/site-finder/analysi
|
||||||
import { Section4Estimate } from "@/components/site-finder/analysis/Section4Estimate";
|
import { Section4Estimate } from "@/components/site-finder/analysis/Section4Estimate";
|
||||||
import { Section5Atmosphere } from "@/components/site-finder/analysis/Section5Atmosphere";
|
import { Section5Atmosphere } from "@/components/site-finder/analysis/Section5Atmosphere";
|
||||||
import { Section6Forecast } from "@/components/site-finder/analysis/Section6Forecast";
|
import { Section6Forecast } from "@/components/site-finder/analysis/Section6Forecast";
|
||||||
import { Section7Concept } from "@/components/site-finder/analysis/Section7Concept";
|
|
||||||
import { useParcelAnalyzeQuery } from "@/lib/site-finder-api";
|
import { useParcelAnalyzeQuery } from "@/lib/site-finder-api";
|
||||||
import type { ParcelAnalysis } from "@/types/site-finder";
|
import type { ParcelAnalysis } from "@/types/site-finder";
|
||||||
|
|
||||||
|
|
@ -219,11 +218,6 @@ export function AnalysisPageContent({ cad }: Props) {
|
||||||
{/* 6. Прогноз и рекомендация — IMPLEMENTED in 958-B3 (§22 forecast) */}
|
{/* 6. Прогноз и рекомендация — IMPLEMENTED in 958-B3 (§22 forecast) */}
|
||||||
<Section6Forecast cad={cad} selectedHorizon={horizon} />
|
<Section6Forecast cad={cad} selectedHorizon={horizon} />
|
||||||
|
|
||||||
{/* 7. Концепция — генеративный дизайн застройки (#1965 Stage 1).
|
|
||||||
Seeded из анализа (geom + financial_estimate), считается по
|
|
||||||
запросу; result lazy-mounted (Leaflet). */}
|
|
||||||
<Section7Concept analysis={analysis} />
|
|
||||||
|
|
||||||
{/* Chat — grounded parcel-chat over the §22 forecast (#958) */}
|
{/* Chat — grounded parcel-chat over the §22 forecast (#958) */}
|
||||||
<ChatPanel cadNum={cad} />
|
<ChatPanel cadNum={cad} />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -370,7 +364,6 @@ const NAV_GROUPS: NavGroup[] = [
|
||||||
{ id: "section-6-6", label: "6.6 Будущее предложение и конкуренты" },
|
{ id: "section-6-6", label: "6.6 Будущее предложение и конкуренты" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ id: "section-7", label: "7. Концепция" },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -1,327 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Section7Concept — «7. Концепция» (epic #1953, #1965 Stage 1).
|
|
||||||
*
|
|
||||||
* 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.
|
|
||||||
*
|
|
||||||
* Inputs are seeded from the analysis the report already has (no re-entry of the
|
|
||||||
* cadastre / polygon):
|
|
||||||
* • parcel polygon ← extractPolygon(analysis.geom_geojson)
|
|
||||||
* • housing_class ← financial_estimate.housing_class_inferred
|
|
||||||
* • development_type← financial_estimate.development_type_inferred
|
|
||||||
* • 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).
|
|
||||||
*
|
|
||||||
* СЗЗ / 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import dynamic from "next/dynamic";
|
|
||||||
import { useMemo, useState } from "react";
|
|
||||||
|
|
||||||
import { HeadlineBar } from "@/components/ui/HeadlineBar";
|
|
||||||
import { Section } from "@/components/analytics/Section";
|
|
||||||
import {
|
|
||||||
ConceptParamsForm,
|
|
||||||
DEFAULT_PARAMS,
|
|
||||||
type ConceptParams,
|
|
||||||
} from "@/components/concept/ConceptParamsForm";
|
|
||||||
import {
|
|
||||||
extractPolygon,
|
|
||||||
polygonAreaSqm,
|
|
||||||
useCreateConcept,
|
|
||||||
type DevelopmentType,
|
|
||||||
type HousingClass,
|
|
||||||
} from "@/lib/concept-api";
|
|
||||||
import type { ParcelAnalysis } from "@/types/site-finder";
|
|
||||||
|
|
||||||
// ConceptVariantsResult hosts a Leaflet placement map — lazy-mount, no SSR, so
|
|
||||||
// it never bloats the main report bundle (loaded only when results exist).
|
|
||||||
const ConceptVariantsResult = dynamic(
|
|
||||||
() =>
|
|
||||||
import("@/components/concept/ConceptVariantsResult").then(
|
|
||||||
(m) => m.ConceptVariantsResult,
|
|
||||||
),
|
|
||||||
{
|
|
||||||
ssr: false,
|
|
||||||
loading: () => (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
height: 200,
|
|
||||||
background: "var(--bg-card-alt)",
|
|
||||||
border: "1px solid var(--border-card)",
|
|
||||||
borderRadius: 12,
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
color: "var(--fg-tertiary)",
|
|
||||||
fontSize: 13,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Загрузка результата…
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const SCROLL_MARGIN = 72;
|
|
||||||
|
|
||||||
// ── Narrowing helpers (inferred values are typed `string` on the analysis) ────
|
|
||||||
|
|
||||||
const HOUSING_CLASSES: readonly HousingClass[] = [
|
|
||||||
"econom",
|
|
||||||
"comfort",
|
|
||||||
"business",
|
|
||||||
];
|
|
||||||
const DEVELOPMENT_TYPES: readonly DevelopmentType[] = [
|
|
||||||
"spot",
|
|
||||||
"mid_rise",
|
|
||||||
"high_rise",
|
|
||||||
];
|
|
||||||
|
|
||||||
function asHousingClass(value: string | undefined): HousingClass | null {
|
|
||||||
return value != null && (HOUSING_CLASSES as readonly string[]).includes(value)
|
|
||||||
? (value as HousingClass)
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function asDevelopmentType(value: string | undefined): DevelopmentType | null {
|
|
||||||
return value != null &&
|
|
||||||
(DEVELOPMENT_TYPES as readonly string[]).includes(value)
|
|
||||||
? (value as DevelopmentType)
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ha = (sqm: number) =>
|
|
||||||
(sqm / 10_000).toLocaleString("ru-RU", { maximumFractionDigits: 2 });
|
|
||||||
|
|
||||||
// ── Props ─────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
/** The /analyze response the report already holds — no re-fetch / re-entry. */
|
|
||||||
analysis: ParcelAnalysis;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Section7Concept ───────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export function Section7Concept({ analysis }: Props) {
|
|
||||||
// Parcel polygon from the analysis geometry (handles Polygon / MultiPolygon /
|
|
||||||
// null → null; MultiPolygon collapses to its first ring per extractPolygon).
|
|
||||||
const polygon = useMemo(
|
|
||||||
() => extractPolygon(analysis.geom_geojson),
|
|
||||||
[analysis.geom_geojson],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Seed form defaults from the analysis: inferred class/type + cadastral value.
|
|
||||||
// financial_estimate is null under the regulatory gate (СЗЗ / ЗОУИТ / нет
|
|
||||||
// зоны) — fall back to the shared DEFAULT_PARAMS (comfort / mid_rise) so a
|
|
||||||
// manual run is still possible.
|
|
||||||
const fin = analysis.financial_estimate;
|
|
||||||
const seededParams = useMemo<ConceptParams>(() => {
|
|
||||||
return {
|
|
||||||
housing_class:
|
|
||||||
asHousingClass(fin?.housing_class_inferred) ??
|
|
||||||
DEFAULT_PARAMS.housing_class,
|
|
||||||
development_type:
|
|
||||||
asDevelopmentType(fin?.development_type_inferred) ??
|
|
||||||
DEFAULT_PARAMS.development_type,
|
|
||||||
target_floors: DEFAULT_PARAMS.target_floors,
|
|
||||||
land_cost_rub:
|
|
||||||
analysis.egrn?.cadastral_value_rub ?? DEFAULT_PARAMS.land_cost_rub,
|
|
||||||
};
|
|
||||||
}, [fin, analysis.egrn?.cadastral_value_rub]);
|
|
||||||
|
|
||||||
const [params, setParams] = useState<ConceptParams>(seededParams);
|
|
||||||
const concept = useCreateConcept();
|
|
||||||
|
|
||||||
const financeUnavailable = fin == null;
|
|
||||||
|
|
||||||
function handleGenerate() {
|
|
||||||
if (!polygon) return;
|
|
||||||
concept.mutate({
|
|
||||||
parcel_geojson: polygon,
|
|
||||||
housing_class: params.housing_class,
|
|
||||||
target_floors: params.target_floors,
|
|
||||||
development_type: params.development_type,
|
|
||||||
land_cost_rub: params.land_cost_rub,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const parcelAreaSqm = polygon ? polygonAreaSqm(polygon) : 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section id="section-7" style={{ scrollMarginTop: SCROLL_MARGIN }}>
|
|
||||||
<HeadlineBar
|
|
||||||
title="7. Концепция"
|
|
||||||
subtitle="Генеративный дизайн застройки: три стратегии размещения с расчётом ТЭП и финансовой модели. Считается по запросу из геометрии и параметров этого участка."
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Collapsed by default (above-the-fold rule) — form + button inside;
|
|
||||||
heavy result is lazy-mounted only after a run. */}
|
|
||||||
<details style={{ marginTop: 12 }}>
|
|
||||||
<summary
|
|
||||||
style={{
|
|
||||||
cursor: "pointer",
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: 600,
|
|
||||||
color: "var(--fg-primary)",
|
|
||||||
padding: "8px 0",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Рассчитать концепцию застройки для участка
|
|
||||||
</summary>
|
|
||||||
|
|
||||||
<div style={{ marginTop: 12 }}>
|
|
||||||
{/* Graceful empty state — geometry missing / unsupported. */}
|
|
||||||
{!polygon ? (
|
|
||||||
<Section title="Концепция застройки">
|
|
||||||
<p
|
|
||||||
style={{
|
|
||||||
margin: 0,
|
|
||||||
fontSize: 13,
|
|
||||||
color: "var(--fg-secondary)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Геометрия участка недоступна — концепцию построить нельзя.
|
|
||||||
Геометрия загружается из НСПД; повторите анализ позже либо
|
|
||||||
воспользуйтесь страницей «Концепция застройки», где границы
|
|
||||||
можно задать вручную.
|
|
||||||
</p>
|
|
||||||
</Section>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{/* СЗЗ / нет финмодели по регламенту — честный баннер, но НЕ
|
|
||||||
тупик: /concepts работает по полигону + параметрам формы
|
|
||||||
независимо от регуляторного гейта financial_estimate. */}
|
|
||||||
{financeUnavailable && (
|
|
||||||
<div
|
|
||||||
role="note"
|
|
||||||
style={{
|
|
||||||
marginBottom: 12,
|
|
||||||
padding: "12px 16px",
|
|
||||||
background: "var(--warn-soft)",
|
|
||||||
border: "1px solid var(--warn)",
|
|
||||||
borderRadius: 12,
|
|
||||||
fontSize: 13,
|
|
||||||
lineHeight: "20px",
|
|
||||||
color: "var(--fg-primary)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Финмодель по регламенту недоступна — на участке ЗОУИТ / СЗЗ
|
|
||||||
либо не определена территориальная зона. Концепцию можно
|
|
||||||
рассчитать вручную: укажите класс, тип застройки и этажность
|
|
||||||
ниже — движок построит варианты по геометрии участка и
|
|
||||||
заданным параметрам.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "minmax(0, 1fr) 320px",
|
|
||||||
gap: 16,
|
|
||||||
alignItems: "start",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Section
|
|
||||||
title="Участок"
|
|
||||||
subtitle="Границы взяты из геометрии анализа — повторно вводить кадастр не нужно."
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
fontSize: 13,
|
|
||||||
color: "var(--fg-primary)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Кадастровый номер {analysis.cad_num} · площадь ≈{" "}
|
|
||||||
{ha(parcelAreaSqm)} га.
|
|
||||||
</div>
|
|
||||||
<p
|
|
||||||
style={{
|
|
||||||
margin: "8px 0 0",
|
|
||||||
fontSize: 12,
|
|
||||||
color: "var(--fg-tertiary)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Параметры по умолчанию подставлены из анализа: класс и тип
|
|
||||||
застройки — из оценки участка, стоимость — из кадастровой
|
|
||||||
стоимости (если есть). Можно скорректировать.
|
|
||||||
</p>
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
<Section
|
|
||||||
title="Параметры проекта"
|
|
||||||
subtitle="Класс, тип застройки и этажность."
|
|
||||||
>
|
|
||||||
<ConceptParamsForm
|
|
||||||
params={params}
|
|
||||||
onChange={setParams}
|
|
||||||
onSubmit={handleGenerate}
|
|
||||||
hasPolygon={polygon != null}
|
|
||||||
isPending={concept.isPending}
|
|
||||||
/>
|
|
||||||
</Section>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Results */}
|
|
||||||
{concept.isError && (
|
|
||||||
<div style={{ marginTop: 16 }}>
|
|
||||||
<Section title="Варианты застройки">
|
|
||||||
<p
|
|
||||||
style={{
|
|
||||||
margin: 0,
|
|
||||||
fontSize: 13,
|
|
||||||
color: "var(--danger)",
|
|
||||||
}}
|
|
||||||
role="alert"
|
|
||||||
>
|
|
||||||
Не удалось рассчитать концепции: {concept.error.message}
|
|
||||||
</p>
|
|
||||||
</Section>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{concept.isPending && (
|
|
||||||
<div style={{ marginTop: 16 }}>
|
|
||||||
<Section title="Варианты застройки">
|
|
||||||
<p
|
|
||||||
style={{
|
|
||||||
margin: 0,
|
|
||||||
fontSize: 13,
|
|
||||||
color: "var(--fg-secondary)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Движок рассчитывает варианты размещения…
|
|
||||||
</p>
|
|
||||||
</Section>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{concept.isSuccess && (
|
|
||||||
<div style={{ marginTop: 24 }}>
|
|
||||||
<ConceptVariantsResult
|
|
||||||
parcel={polygon}
|
|
||||||
variants={concept.data.variants}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Loading…
Add table
Reference in a new issue