Multi-region audit vs the prototype styles.css/dashboard.html → 22 CSS fixes +
markup. Headline: .lowerGrid/.bottomGrid align-items:start so the sparse ОКС
«нет данных» card no longer stretches into a huge void. Plus restore missing
prototype bits: topbar icon-buttons (Экспорт→Отчёты, Полный экран→fullscreen,
Настройки aria-disabled «скоро»), rail help-dot, app-footer (honest disclaimer,
no fabricated model/timestamp), invest-score sparkline (decorative, aria-hidden),
green/amber value tones, engineering status-row dots (from real nearest_m),
verdict-card gradient, scan-card status-rows, .canvas/.hbar/.mixHead spacing.
All new CSS scoped under .pticaRoot[data-theme=dark] (status-row classes
descendant-scoped under .scanCard — no collision with the drawer's). Environment
card stays honest kvrow placeholders (no fake dots). a11y: aria-labels on icon
buttons. TS strict, no any. tsc/lint/prettier/build green. code-reviewer ✅.
108 lines
3.5 KiB
TypeScript
108 lines
3.5 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 { PticaValueTone } from "@/components/site-finder/ptica/ptica-adapt";
|
||
import type { DrawerKey } from "@/components/site-finder/ptica/drawers/drawer-keys";
|
||
|
||
/** Decorative hero sparkline shape (prototype data-spark="4,5,6,5,7,6,8,7,9,8"). */
|
||
const SPARK = [4, 5, 6, 5, 7, 6, 8, 7, 9, 8];
|
||
const SPARK_MAX = Math.max(...SPARK);
|
||
|
||
function toneClass(tone: PticaValueTone): string {
|
||
if (tone === "good") return styles.vGood;
|
||
if (tone === "warn") return styles.vWarn;
|
||
return "";
|
||
}
|
||
|
||
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 className={styles.sparkline} aria-hidden="true">
|
||
{SPARK.map((v, i) => (
|
||
<span
|
||
key={i}
|
||
style={{ height: `${Math.round((v / SPARK_MAX) * 100)}%` }}
|
||
/>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
<div className={styles.statLine}>
|
||
<span className={styles.k}>Потенциал</span>
|
||
<span
|
||
className={`${styles.v} ${toneClass(inv.potentialTone)}`}
|
||
title={inv.potential.caption}
|
||
>
|
||
{inv.potential.value}
|
||
</span>
|
||
</div>
|
||
<div className={styles.statLine}>
|
||
<span className={styles.k}>Риск</span>
|
||
<span
|
||
className={`${styles.v} ${toneClass(inv.riskTone)}`}
|
||
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} ${styles.detailBtnScan}`}
|
||
onClick={() => onOpenDrawer("potential")}
|
||
title="Открыть расчёт ёмкости"
|
||
>
|
||
Подробнее
|
||
</button>
|
||
</div>
|
||
);
|
||
}
|