gendesign/tradein-mvp/frontend/src/components/trade-in/v2/CacheView.tsx
bot-backend 91b460b157
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI / changes (pull_request) Successful in 8s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Successful in 1m13s
CI Trade-In / frontend-checks (pull_request) Successful in 1m19s
CI / openapi-codegen-check (pull_request) Successful in 2m20s
chore(frontend): фикстуры макета не по умолчанию + удаление осиротевших компонентов
Волна 0 инвентаризации техдолга.

МЕРА: у восьми компонентов витрины v2 проп data имел значение по
умолчанию из fixtures.ts — sample-данных дизайн-макета. При сбое
передачи данных компонент отрисовал бы выдуманные числа вместо
пустого состояния; на платном экране оценки это неотличимо от
правды. Проп стал обязательным. Цепная правка в SectionOverlay:
его опциональные поля пробрасывались в теперь-обязательные пропы.

Проверено: единственный потребитель — app/v2/page.tsx, всегда
передаёт data явно. Отдельной preview-страницы у v2/fixtures.ts
нет. Статический UI-конфиг из того же файла (лейблы, версия,
опции) не тронут — он не про данные отчёта.

Птица: удалены шесть осиротевших компонентов (ScoreCard,
AnalysisBreadcrumb, AnalysisSidebar, MassingEconomics, UserAvatar,
PticaPlaceholderPanel) — по каждому подтверждён ноль импортов по
всему репозиторию, включая динамические. Подчищены два
ссылающихся комментария.

Проверка: tsc и eslint чисто на обоих фронтах, next build успешен,
vitest Птицы 264 теста зелёные.
2026-08-06 20:19:59 +03:00

248 lines
8.9 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.

// OVERLAY 07: ПРЕДЫДУЩИЕ ОЦЕНКИ (cache view).
// Faithful markup port of МЕРА Оценка.dc.html lines 593-613.
// Data via the required `data` prop (mapCache output).
import { tokens } from "./tokens";
import type { CacheData } from "./mappers";
/**
* M8 — ВРЕМЯ column normalizer. `r.time` arrives PRE-FORMATTED as a string from
* mapCache (CacheRow.time, produced by fmtCacheTime), one of:
* • same-day → "HH:MM:SS" (e.g. "23:20:51")
* • yesterday → "вчера HH:MM"
* • older → "DD.MM.YYYY"
* The raw column mixed second-precision clock times with bare dates, so a lone
* "23:20:51" was visually indistinguishable from a date. We normalize every bare
* clock time to minute precision (HH:MM) so same-day rows read uniformly with the
* "вчера HH:MM" rows, while date rows pass through unchanged. The ISO timestamp is
* already discarded upstream (mapper not editable here), so a same-day cell stays
* a time by the column's convention: a clock value ⇒ today, a date ⇒ older.
* Missing / unparseable → «—». Pure + idempotent on its own output.
*/
function normTime(raw: string | null | undefined): string {
const s = (raw ?? "").trim();
if (!s || s === "—") return "—";
// Bare clock time "HH:MM:SS" | "HH:MM" → minute precision (drop seconds).
const clock = s.match(/^(\d{1,2}):(\d{2})(?::\d{2})?$/);
if (clock) return `${clock[1].padStart(2, "0")}:${clock[2]}`;
// "вчера HH:MM:SS" → "вчера HH:MM"; dates ("DD.MM.YYYY") have no HH:MM:SS run
// and pass through unchanged.
return s.replace(/(\d{1,2}:\d{2}):\d{2}\b/, "$1");
}
interface CacheViewProps {
// Required on the app path (v2/page.tsx -> SectionOverlay always supplies
// mapCache output) — an omitted prop must be a TS error, not a silent
// fallback to fabricated «Предыдущие оценки» rows.
data: CacheData;
/** Row-click handler (#2420) — navigates to the full report for that
* historical estimate. Optional: leaves rows inert when absent. */
onSelectRow?: (id: string) => void;
}
export function CacheView({ data, onSelectRow }: CacheViewProps) {
const showSrc = data.rows.some((r) => r.src && r.src !== "—");
// #2420 — ПАРАМЕТРЫ column inserted right after АДРЕС.
const gridCols = showSrc
? "2.2fr 1.3fr 1fr 1fr 1fr"
: "2.2fr 1.3fr 1fr 1fr";
// M7 — header cells, in column order, so every data column has a header.
const headers = showSrc
? ["АДРЕС", "ПАРАМЕТРЫ", "ВРЕМЯ", "ИСТОЧНИКОВ", "СТАТУС"]
: ["АДРЕС", "ПАРАМЕТРЫ", "ВРЕМЯ", "СТАТУС"];
return (
<div style={{ display: "flex", flexDirection: "column", gap: 22 }}>
{/* #2420 — clickable-row affordance: hover tint + visible focus ring,
mirroring .sv-row:hover (SourcesView) / focus-visible rings
(LeadForm/ParamsPanel). Only meaningful when onSelectRow is wired. */}
<style>{`
.tiv2-cache-row:hover { background: ${tokens.surface.w50}; }
.tiv2-cache-row:focus-visible {
outline: none;
box-shadow: inset 0 0 0 2px ${tokens.accentDeep};
}
`}</style>
{/* 3 KPI cards */}
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(3,1fr)",
gap: 16,
}}
>
{data.kpis.map((kpi) => (
<div
key={kpi.label}
style={{
background: tokens.surface.w55,
border: `1px solid ${tokens.line2}`,
borderRadius: 8,
padding: "16px 18px",
}}
>
<div
style={{
fontSize: 9.5,
letterSpacing: 1.5,
// C1 — key caption (labels the headline KPI value) → body2.
color: tokens.body2,
}}
>
{kpi.label}
</div>
<div
style={{
fontFamily: tokens.font.mono,
fontSize: 30,
fontWeight: 300,
color: tokens.ink2,
margin: "8px 0 5px",
}}
>
{kpi.value}
{kpi.unit && (
<>
{kpi.unit !== "%" && " "}
<span style={{ fontSize: 14, color: tokens.muted }}>
{kpi.unit}
</span>
</>
)}
</div>
<div style={{ fontSize: 10, color: tokens.hint }}>{kpi.sub}</div>
</div>
))}
</div>
{/* Последние оценки table */}
<div
style={{
background: tokens.surface.w55,
border: `1px solid ${tokens.line2}`,
borderRadius: 8,
overflow: "hidden",
}}
>
<div
style={{
padding: "14px 18px",
borderBottom: `1px solid ${tokens.lineSoft}`,
fontSize: 12,
fontWeight: 600,
color: tokens.ink2,
}}
>
Последние оценки
</div>
{/* M7 — real table semantics via ARIA roles over the existing grid. */}
<div role="table" aria-label="Последние оценки">
<div
role="row"
style={{
display: "grid",
gridTemplateColumns: gridCols,
gap: 10,
padding: "9px 18px",
fontSize: 9,
letterSpacing: 1,
// C1 — column headers are primary labels → body2.
color: tokens.body2,
borderBottom: `1px solid ${tokens.lineSoft2}`,
}}
>
{headers.map((h) => (
<span key={h} role="columnheader">
{h}
</span>
))}
</div>
{data.rows.map((r, i) => {
// #2420 — clicking/activating a row navigates to the full report
// for that historical estimate (SectionOverlay -> page.tsx). Rows
// stay a passive role="row"/role="cell" ARIA-grid когда onSelectRow
// не задан (unwired/storybook usage), меняя только tabIndex/handlers
// — the grid structure itself is unchanged.
const clickable = Boolean(onSelectRow);
function activate() {
onSelectRow?.(r.id);
}
return (
<div
key={`${r.addr}-${i}`}
role="row"
className={clickable ? "tiv2-cache-row" : undefined}
tabIndex={clickable ? 0 : undefined}
onClick={clickable ? activate : undefined}
onKeyDown={
clickable
? (e) => {
if (e.key === "Enter" || e.key === " ") {
if (e.key === " ") e.preventDefault();
activate();
}
}
: undefined
}
aria-label={clickable ? `Открыть отчёт: ${r.addr}` : undefined}
style={{
display: "grid",
gridTemplateColumns: gridCols,
gap: 10,
padding: "12px 18px",
fontSize: 11,
borderBottom: `1px solid ${tokens.lineSoft3}`,
alignItems: "center",
cursor: clickable ? "pointer" : undefined,
}}
>
<span role="cell" style={{ color: tokens.body }}>
{r.addr}
</span>
<span role="cell" style={{ color: tokens.body }}>
{r.params}
</span>
<span
role="cell"
style={{ fontFamily: tokens.font.mono, color: tokens.muted2 }}
>
{normTime(r.time)}
</span>
{showSrc && (
<span
role="cell"
style={{
fontFamily: tokens.font.mono,
color: tokens.muted,
}}
>
{r.src}
</span>
)}
<span
role="cell"
style={{
display: "flex",
alignItems: "center",
gap: 6,
fontSize: 10,
color: r.statusColor,
}}
>
<span
style={{
width: 7,
height: 7,
borderRadius: "50%",
background: r.statusColor,
}}
/>
{r.status}
</span>
</div>
);
})}
</div>
</div>
</div>
);
}