diff --git a/tradein-mvp/frontend/src/app/v2/page.tsx b/tradein-mvp/frontend/src/app/v2/page.tsx index 8d3f4795..d6a2eca1 100644 --- a/tradein-mvp/frontend/src/app/v2/page.tsx +++ b/tradein-mvp/frontend/src/app/v2/page.tsx @@ -920,6 +920,14 @@ export default function TradeInV2Page() { analytics={analyticsViewData} sources={sourcesData} cache={cacheData} + onSelectEstimate={(id) => { + // #2420 — same ?id= restore-by-id shape as handleSubmit/handleNew + // above (line ~649); this file uses replace() consistently for + // every /v2?id= transition, so we follow that convention here + // too rather than introducing the file's only push() call. + router.replace(`/v2?id=${id}`, { scroll: false }); + setNav(0); + }} /> )} void; +} + +export function CacheView({ data = FIXTURE_CACHE, onSelectRow }: CacheViewProps) { const showSrc = data.rows.some((r) => r.src && r.src !== "—"); - const gridCols = showSrc ? "2.4fr 1fr 1fr 1fr" : "2.4fr 1fr 1fr"; + // #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 (
+ {/* #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. */} + {/* 3 KPI cards */}
))}
- {data.rows.map((r, i) => ( -
- - {r.addr} - - - {normTime(r.time)} - - {showSrc && ( - - {r.src} - - )} - { + // #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 ( +
{ + if (e.key === "Enter" || e.key === " ") { + if (e.key === " ") e.preventDefault(); + activate(); + } + } + : undefined + } + aria-label={clickable ? `Открыть отчёт: ${r.addr}` : undefined} style={{ - display: "flex", + display: "grid", + gridTemplateColumns: gridCols, + gap: 10, + padding: "12px 18px", + fontSize: 11, + borderBottom: `1px solid ${tokens.lineSoft3}`, alignItems: "center", - gap: 6, - fontSize: 10, - color: r.statusColor, + cursor: clickable ? "pointer" : undefined, }} > + + {r.addr} + + + {r.params} + + {normTime(r.time)} + + {showSrc && ( + + {r.src} + + )} + - {r.status} - -
- ))} + > + + {r.status} + +
+ ); + })}
diff --git a/tradein-mvp/frontend/src/components/trade-in/v2/SectionOverlay.tsx b/tradein-mvp/frontend/src/components/trade-in/v2/SectionOverlay.tsx index 1063e6b9..8b0ea3d5 100644 --- a/tradein-mvp/frontend/src/components/trade-in/v2/SectionOverlay.tsx +++ b/tradein-mvp/frontend/src/components/trade-in/v2/SectionOverlay.tsx @@ -28,6 +28,10 @@ interface SectionOverlayProps { sources?: SourcesData; analytics?: Analytics; cache?: CacheData; + // #2420 — «ПРЕДЫДУЩИЕ ОЦЕНКИ» row click: navigate to the full report for + // that historical estimate. Optional (threaded straight to CacheView's own + // optional onSelectRow — unwired usage leaves rows inert). + onSelectEstimate?: (id: string) => void; } // Panel geometry (relative to the artboard). Kept as a constant so the click @@ -43,6 +47,7 @@ export default function SectionOverlay({ sources, analytics, cache, + onSelectEstimate, }: SectionOverlayProps) { // Non-modal dialog plumbing. `dialogRef` is the dialog root, `lastFocused` // keeps the element that had focus before the overlay opened so we can restore @@ -316,7 +321,9 @@ export default function SectionOverlay({ {active === 3 && ( )} - {active === 4 && } + {active === 4 && ( + + )} diff --git a/tradein-mvp/frontend/src/components/trade-in/v2/fixtures.ts b/tradein-mvp/frontend/src/components/trade-in/v2/fixtures.ts index ad68e530..4471b184 100644 --- a/tradein-mvp/frontend/src/components/trade-in/v2/fixtures.ts +++ b/tradein-mvp/frontend/src/components/trade-in/v2/fixtures.ts @@ -536,46 +536,58 @@ export const analytics: Analytics = { export const cacheRows: CacheRow[] = [ { + id: "fixture-1", addr: "ул. Малышева, 30", time: "12:45:02", src: "3 / 7", status: "свежий", statusColor: tokens.success, + params: "2 комн · 54 м² · 5/9 эт.", }, { + id: "fixture-2", addr: "ул. Куйбышева, 48", time: "12:31:50", src: "4 / 7", status: "свежий", statusColor: tokens.success, + params: "3 комн · 72 м² · 12/16 эт.", }, { + id: "fixture-3", addr: "ул. Академика Ландау, 35", time: "11:58:14", src: "2 / 7", status: "свежий", statusColor: tokens.success, + params: "1 комн · 38 м²", }, { + id: "fixture-4", addr: "ул. 8 Марта, 120", time: "09:12:40", src: "3 / 7", status: "устаревает", statusColor: tokens.warn, + params: "студия · 24 м² · 3/5 эт.", }, { + id: "fixture-5", addr: "пр. Ленина, 5", time: "вчера 18:04", src: "5 / 7", status: "устарел", statusColor: tokens.danger, + params: "2 комн · 61,5 м²", }, { + id: "fixture-6", addr: "ул. Белинского, 86", time: "вчера 14:22", src: "2 / 7", status: "устарел", statusColor: tokens.danger, + params: "4 комн · 95 м² · 7/10 эт.", }, ]; diff --git a/tradein-mvp/frontend/src/components/trade-in/v2/mappers.ts b/tradein-mvp/frontend/src/components/trade-in/v2/mappers.ts index 3025a402..b2afe279 100644 --- a/tradein-mvp/frontend/src/components/trade-in/v2/mappers.ts +++ b/tradein-mvp/frontend/src/components/trade-in/v2/mappers.ts @@ -2002,6 +2002,30 @@ function fmtCacheTime(iso: string | null | undefined): string { return fmtDate(iso); } +/** + * #2420 — ПАРАМЕТРЫ column: "2 комн · 54 м²" / "2 комн · 54 м² · 5/9 эт.". + * rooms + area are independent (each omitted if unknown); floor segment ONLY + * appended when BOTH floor and total_floors are known (a lone floor without a + * total is misleading — omitted entirely, no "5/? эт."). "—" when neither + * rooms nor area is available (mirrors `addr: h.address ? … : "—"` above). + */ +function cacheParams( + rooms: number | null, + areaM2: string | null, + floor: number | null, + totalFloors: number | null, +): string { + const area = areaM2 != null ? Number(areaM2) : null; + const parts: string[] = []; + if (rooms != null) parts.push(rooms <= 0 ? "студия" : `${rooms} комн`); + if (area != null && Number.isFinite(area)) parts.push(fmtArea(area)); + if (parts.length === 0) return "—"; + if (floor != null && totalFloors != null) { + parts.push(`${floor}/${totalFloors} эт.`); + } + return parts.join(" · "); +} + /** Свежесть оценки vs 24ч TTL кэша → подпись + token-цвет точки/текста. */ function cacheStatus(iso: string | null | undefined): { status: string; @@ -2024,7 +2048,7 @@ function cacheStatus(iso: string | null | undefined): { /** * 07 ПРЕДЫДУЩИЕ ОЦЕНКИ: таблица последних оценок + 3 KPI, всё посчитано на * фронте из per-user /history (null → пустая таблица + KPI с "—"). - * rows — addr/время/статус по каждой строке (src всегда "—", TODO BE-1). + * rows — addr/время/статус/params по каждой строке (src всегда "—", TODO BE-1). * kpis — ВСЕГО ОЦЕНОК (length) · СРЕДНЯЯ ЦЕНА (avg median_price, млн). * «ПОВТОРНЫЕ АДРЕСА» убран (§M8 — внутренняя ops-метрика). */ @@ -2034,6 +2058,7 @@ export function mapCache(history: EstimateHistoryItem[] | null): CacheData { const rows: CacheRow[] = list.map((h) => { const { status, statusColor } = cacheStatus(h.created_at); return { + id: h.id, // §M8 — de-glue metro/address runs the same way as the analog/deal rows // (these are the user's own past-estimate addresses, often geocoded with // metro proximity jammed on with no separators). @@ -2042,6 +2067,7 @@ export function mapCache(history: EstimateHistoryItem[] | null): CacheData { src: "—", // TODO BE-1: /history без sources_used → N/7 недоступно status, statusColor, + params: cacheParams(h.rooms, h.area_m2, h.floor, h.total_floors), }; }); diff --git a/tradein-mvp/frontend/src/components/trade-in/v2/types.ts b/tradein-mvp/frontend/src/components/trade-in/v2/types.ts index 15de1a55..c567db8c 100644 --- a/tradein-mvp/frontend/src/components/trade-in/v2/types.ts +++ b/tradein-mvp/frontend/src/components/trade-in/v2/types.ts @@ -242,12 +242,16 @@ export interface Analytics { // ---- OVERLAY: ПРЕДЫДУЩИЕ ОЦЕНКИ (CACHE) ----------------------------------- export interface CacheRow { + /** Estimate UUID — needed to navigate to the full report on row click (#2420). */ + id: string; addr: string; time: string; src: string; status: string; /** Status dot/text colour (success / warn / danger token value). */ statusColor: string; + /** Pre-formatted compact summary, e.g. "2 комн · 54 м² · 5/9 эт." (#2420). */ + params: string; } export interface CacheKpi { diff --git a/tradein-mvp/frontend/src/types/trade-in.ts b/tradein-mvp/frontend/src/types/trade-in.ts index b60aa368..6ae8fc38 100644 --- a/tradein-mvp/frontend/src/types/trade-in.ts +++ b/tradein-mvp/frontend/src/types/trade-in.ts @@ -423,6 +423,13 @@ export interface EstimateHistoryItem { confidence: ConfidenceLevel; n_analogs: number; created_at: string; // ISO datetime + // #2420 — вернулись в GET /history наравне с остальными параметрами оценки. + floor: number | null; + total_floors: number | null; + year_built: number | null; + house_type: string | null; + repair_state: string | null; + has_balcony: boolean | null; } // ── Geocode suggest (endpoint: GET /geocode/suggest?q=&limit=) ──