gendesign/frontend/src/components/site-finder/ptica/cockpit/InsolationCard.tsx
Light1YT 58e71dab01 fix(report): surface ПЗЗ-градрегламент НСПД в Раздел 1 (#1962)
Light report (Section1ParcelInfo) only rendered the dead top-level
zoning.data_available=false (closed-PKK6 path) and never showed the
resolved nspd_zoning, so the resolved regulation (synth #1891) was
invisible to the user («мы выгружали НСПД — где они?»).

- Add «Градрегламент (НСПД)»-card to Раздел 1 (right column, под ЕГРН):
  тер.зона, КСИТ/max_far, пред. высота, этажность, коэф. застройки,
  КСИТ-ёмкость/пятно (от площади ЕГРН), источник + список разрешённых
  ВРИ зоны. Empty-state когда регламент не определён (gap-зоны / не-ЕКБ).
  КСИТ-microcopy расшифровывает жаргон на первой странице.
- New pure mapper adaptNspdRegulation (nspd-regulation.ts) reused by
  ptica-adapt so light report и /ptica остаются consistent.
- coerceFloat: max_far/max_building_pct приходят с провода СТРОКАМИ
  ("2.4"/"80") — фикс латентного бага cockpit'а (typeof===number
  глушил реальный строковый КСИТ в massing/insolation).
- Widen NspdZoning.max_far/max_building_pct to string|number, add
  main_vri; expose nspd_zoning on ParcelAnalyzeResponse.
- Unit test для field-mapping + coercion + empty-state.
2026-06-27 17:10:20 +05:00

57 lines
2.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
/**
* InsolationCard — lower-grid «Инсоляционная матрица» preview card. Renders a
* LIVE compact autorotating 3D massing model (a hands-off mini version of the
* «Открыть 3D» drawer) instead of the old abstract SVG cube, fed by the REAL
* parcel area (geometry) + НСПД КСИТ (max_far). «Открыть 3D» opens the
* insolation drawer (3D massing + shadow module).
*/
import styles from "@/app/site-finder/analysis/[cad]/ptica/ptica.module.css";
import type { ParcelAnalysis } from "@/types/site-finder";
import { coerceFloat } from "@/components/site-finder/analysis/nspd-regulation";
import { parcelAreaM2 } from "@/components/site-finder/ptica/ptica-adapt";
import { MassingViewer } from "@/components/site-finder/ptica/massing/MassingViewer";
import type { DrawerKey } from "@/components/site-finder/ptica/drawers/drawer-keys";
interface Props {
analysis: ParcelAnalysis;
onOpenDrawer: (key: DrawerKey) => void;
}
export function InsolationCard({ analysis, onOpenDrawer }: Props) {
const areaM2 = parcelAreaM2(analysis) ?? undefined;
// max_far arrives as a string on the wire — coerce before passing to the 3D viewer.
const maxFar = coerceFloat(analysis.nspd_zoning?.max_far) ?? undefined;
const farIsReal = maxFar != null;
return (
<div className={styles.card}>
<div className={styles.cardHead}>
<h3 className={styles.cardTitle}>Инсоляционная матрица</h3>
<span className={styles.openTag}>превью</span>
</div>
<MassingViewer
preview
areaM2={areaM2}
maxFar={maxFar}
farIsReal={farIsReal}
/>
<div className={styles.placeholder}>
<div className={styles.placeholderSoon}>ПРЕВЬЮ</div>
<p>Тени по времени суток в 3D-модуле массы застройки.</p>
<button
type="button"
className={styles.detailBtn}
onClick={() => onOpenDrawer("insolation")}
title="Открыть 3D-модуль"
>
Открыть 3D
</button>
</div>
</div>
);
}