gendesign/frontend/src/components/site-finder/ptica/InvestScoreBlock.tsx
Light1YT fcaa4d197b
All checks were successful
CI / changes (pull_request) Successful in 7s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Successful in 59s
CI / openapi-codegen-check (pull_request) Successful in 1m57s
fix(ptica): visual parity 1:1 with prototype (incl. ОКС empty-card stretch)
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 .
2026-06-21 01:10:36 +05:00

108 lines
3.5 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";
/**
* 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>
);
}