New non-destructive route /site-finder/analysis/[cad]/ptica rendering the existing /analyze data in the «ПТИЦА» operator-terminal cockpit. The light analysis page, Section1-6, globals.css and package.json are untouched. PR#1 scope: app-shell (topbar + 4 tabs + left rail), hero (dark Leaflet map + parcel passport KV-grid + Buildability radial gauge + invest-score block), and the Development Scan card grid — wired to useParcelAnalyzeQuery / ParcelAnalysis. Data honesty: real fields render live (passport egrn.*, district, utilities.summary, median_price, pipeline_24mo); absent fields show muted placeholders with source captions (buildability/risk = derived proxy «предв.»; invest/buy-signal «после прогноза»; КСИТ/height/ОКС/economy «—») via typed ptica-adapt.ts — no fake numbers presented as live. Dark theme is a CSS-module scoped under .pticaRoot[data-theme=dark] (cannot leak to the light app); IBM Plex Mono added via next/font (no dep/lockfile change). Leaflet via dynamic(ssr:false). TS strict, no any. Deferred to follow-up PRs: Scenarios (forecast), Reports+Compare tabs, 16 detail drawers, Three.js 3D massing.
81 lines
2.3 KiB
TypeScript
81 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}>
|
|
<div
|
|
className={styles.cardTitle}
|
|
style={{ fontSize: "9.5px", textAlign: "center" }}
|
|
>
|
|
{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>
|
|
);
|
|
}
|