gendesign/frontend/src/components/site-finder/ptica/BuildabilityGauge.tsx
Light1YT 24b5e6047f
All checks were successful
CI / changes (pull_request) Successful in 6s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Successful in 1m6s
CI / openapi-codegen-check (pull_request) Successful in 2m1s
feat(ptica): complete cockpit lower-grid + bottom-grid sections
Port the prototype's missing sections — the React cockpit had only hero +
Development Scan, so the deployed page was incomplete («поплывшее»). Add:
- lower-grid: ОКС · Development Potential · Recommended Product (квартирография)
  · Инсоляционная матрица (opens 3D);
- bottom-grid: Investment Clearance · Buy Signal · Legal Status · Site Verdict
  («Приобретать/Нельзя/Нужна проверка» plaque + checklist).
Move ОКС + Инсоляция out of DevelopmentScan → scan = 7 cards (prototype IA).

Data honesty: Development Potential (real КСИТ/площадь/плотность), Recommended
Product (forecast product_tz), Buy Signal + Site Verdict (forecast deficit +
gate_verdict, real, three-state), Legal (zouit/red_lines/gate). Investment
Clearance all «—» «после финмодели (§23)» — no fabricated numbers. Typed
{value,isReal,caption} adapters; scoped dark CSS; forecast-pending safe.
Cleanup: drop orphaned adaptOksCard/adaptInsolationCard; add .metric/.bignum.
2026-06-21 00:33:15 +05:00

80 lines
2.3 KiB
TypeScript

"use client";
/**
* BuildabilityGauge — reusable SVG radial gauge for the cockpit.
*
* Renders a 0..100 value (or a muted track when value is null) with a color
* keyed to the `tone` bucket via CSS-module classes (which resolve to dark
* tokens). Optional footnote (e.g. "предв.") flags soft/preliminary proxies.
*/
import styles from "@/app/site-finder/analysis/[cad]/ptica/ptica.module.css";
import type { PticaGauge } from "@/components/site-finder/ptica/ptica-adapt";
const RADIUS = 52;
const CIRCUMFERENCE = 2 * Math.PI * RADIUS;
const PROG_CLASS: Record<PticaGauge["tone"], string> = {
good: styles.gaugeGood,
warn: styles.gaugeWarn,
bad: styles.gaugeBad,
neutral: styles.gaugeNeutral,
};
const NUM_CLASS: Record<PticaGauge["tone"], string> = {
good: styles.gaugeNumGood,
warn: styles.gaugeNumWarn,
bad: styles.gaugeNumBad,
neutral: "",
};
interface Props {
gauge: PticaGauge;
/** Title shown above the gauge (e.g. "Buildability Index"). */
title: string;
}
export function BuildabilityGauge({ gauge, title }: Props) {
const { value, label, tone, footnote } = gauge;
const pct = value ?? 0;
const dashOffset = CIRCUMFERENCE * (1 - pct / 100);
return (
<div className={styles.gaugePanel}>
{title && (
<div className={`${styles.cardTitle} ${styles.cardTitleGauge}`}>
{title}
</div>
)}
<div className={styles.gauge}>
<svg viewBox="0 0 120 120" aria-hidden="true">
<circle
className={styles.gaugeTrack}
cx="60"
cy="60"
r={RADIUS}
strokeWidth="9"
/>
{value != null && (
<circle
className={`${styles.gaugeProg} ${PROG_CLASS[tone]}`}
cx="60"
cy="60"
r={RADIUS}
strokeWidth="9"
strokeDasharray={CIRCUMFERENCE}
strokeDashoffset={dashOffset}
/>
)}
</svg>
<div className={styles.gaugeCenter}>
<div className={`${styles.gaugeNum} ${NUM_CLASS[tone]}`}>
{value != null ? `${value}%` : "—"}
</div>
<div className={styles.gaugeLab}>{label}</div>
</div>
</div>
{footnote && <div className={styles.gaugeFootnote}>{footnote}</div>}
</div>
);
}