PticaDrawer: controlled right slider (480px / full-screen <768), scrim+Esc close, focus-on-open, body-scroll-lock (restored on close/unmount), dialog a11y. Deep-link ?drawer= via useSearchParams + shallow router.replace (no render loop; unknown keys ignored). Enables the «Подробнее» buttons on scan + hero cards. 17 drawer contents (passport/buildability/urban/restrictions/engineering/ market[4 sub-tabs]/economy/risks/georisks/oks/potential/product/finance/legal/ environment/insolation/future3d) built from ParcelAnalysis + forecast product_tz. Many are REAL: environment (noise/air_quality/wind/weather), georisks (geology/ hydrology/geotech_risk), market (competitors/market_trend/velocity/pipeline), restrictions (zouit/red_lines/zoning), engineering, passport. Absent fields (КСИТ/ОКС/финмодель/insolation) show honest «—»/«скоро» via ptica-adapt. All field paths verified against types. Dark theme scoped. TS strict, no any. Follow-up: drawer focus-trap + focus-restore for full modal a11y.
83 lines
2.7 KiB
TypeScript
83 lines
2.7 KiB
TypeScript
"use client";
|
||
|
||
/**
|
||
* InvestScoreBlock — hero score panel (col 3): big /10 invest-score number + a
|
||
* buy-signal badge. In INCREMENT 1 the invest-score and buy-signal are
|
||
* PLACEHOLDERS (surfaced after the §22 forecast / Scenarios), so the big number
|
||
* renders muted "—" with a caption instead of a fabricated value.
|
||
*/
|
||
|
||
import styles from "@/app/site-finder/analysis/[cad]/ptica/ptica.module.css";
|
||
import type { ParcelAnalysis } from "@/types/site-finder";
|
||
import type { ForecastReport } from "@/types/forecast";
|
||
import { adaptInvestScore } from "@/components/site-finder/ptica/ptica-adapt";
|
||
import type { DrawerKey } from "@/components/site-finder/ptica/drawers/drawer-keys";
|
||
|
||
interface Props {
|
||
analysis: ParcelAnalysis;
|
||
/** §22 forecast report — when ready, surfaces the REAL invest-score / buy-signal. */
|
||
forecastReport?: ForecastReport;
|
||
onOpenDrawer: (key: DrawerKey) => void;
|
||
}
|
||
|
||
export function InvestScoreBlock({
|
||
analysis,
|
||
forecastReport,
|
||
onOpenDrawer,
|
||
}: Props) {
|
||
const inv = adaptInvestScore(analysis, forecastReport);
|
||
|
||
return (
|
||
<div className={styles.scoreStack}>
|
||
<div>
|
||
<div className={styles.statLine}>
|
||
<span className={styles.k}>Инвест-скор</span>
|
||
</div>
|
||
<div className={styles.investScore}>
|
||
{inv.score.isReal ? (
|
||
<>
|
||
<span className={styles.investBig}>{inv.score.value}</span>
|
||
<span className={styles.investSmall}>/ 10</span>
|
||
</>
|
||
) : (
|
||
<span className={`${styles.investBig} ${styles.investPlaceholder}`}>
|
||
—
|
||
</span>
|
||
)}
|
||
</div>
|
||
{inv.score.caption && (
|
||
<span className={styles.fieldCaption}>{inv.score.caption}</span>
|
||
)}
|
||
</div>
|
||
|
||
<div className={styles.statLine}>
|
||
<span className={styles.k}>Потенциал</span>
|
||
<span className={styles.v} title={inv.potential.caption}>
|
||
{inv.potential.value}
|
||
</span>
|
||
</div>
|
||
<div className={styles.statLine}>
|
||
<span className={styles.k}>Риск</span>
|
||
<span className={styles.v} title={inv.risk.caption}>
|
||
{inv.risk.value}
|
||
</span>
|
||
</div>
|
||
|
||
<div className={styles.statLine}>
|
||
<span className={styles.k}>Buy Signal</span>
|
||
<span className={styles.badge} title={inv.buySignal.caption}>
|
||
{inv.buySignal.isReal ? inv.buySignal.value : "после прогноза"}
|
||
</span>
|
||
</div>
|
||
|
||
<button
|
||
type="button"
|
||
className={styles.detailBtn}
|
||
onClick={() => onOpenDrawer("potential")}
|
||
title="Открыть расчёт ёмкости"
|
||
>
|
||
Подробнее
|
||
</button>
|
||
</div>
|
||
);
|
||
}
|