All checks were successful
CI / changes (pull_request) Successful in 8s
CI Trade-In / changes (pull_request) Successful in 8s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m5s
Живой визуальный аудит /trade-in/v2 нашёл HIGH/MEDIUM/LOW дефекты после honest-hero раунда — фиксы: - H1: честный fallback-state флагманской карточки при expected_sold_price=null (вместо четырёх прочерков) - H3: две расходящиеся "медианы объявлений" на одном экране (рейка 14.23млн vs герой 8.99млн на тех же аналогах) — унифицированы на e.median_price_rub - M1-M3: иерархия трёх цен (asking уменьшен, круговой гейдж → пилюля "-N% к цене объявления", дисклеймер разброса читаем) - M4: PDF-отчёт primary после результата (без добавления lead-CTA — открытый продуктовый вопрос, не решался) - M5: порядок меток оси X графика (2026 в начале, не терялась при years!=5) — geometry вынесена в общий export (phYearX) вместо дублирования в двух файлах - M6: gate немонотонного "срок продажи" при n<5 аналогов - M7: сброс scroll оверлея при смене секции - M8 + LOW: унификация терминов (СТОИМОСТЬ→ЦЕНА, CV→"разброс цен", P25-P75 человекочитаемо, ДКП-расшифровка, скрыт литеральный "Обновлено: —", метро-разделители в адресе, убран внутренний "ПОВТОРНЫЕ АДРЕСА" label) - Контраст accent-текста hero (40px headline) поднят с ~2.87:1 до ~4.2:1 (accent → accentDeep, существующий токен той же палитры) Мобильная раскладка (H2) вынесена в отдельный issue #2275, не в этом PR. Lead-CTA "Оставить заявку" — открытый продуктовый вопрос, не решался. Refs #2266, #2262
369 lines
11 KiB
TypeScript
369 lines
11 KiB
TypeScript
// 03 СВОДКА ОБЪЕКТА — right-rail summary card for the /trade-in/v2 "МЕРА Оценка"
|
||
// design port. Faithful markup port of МЕРА Оценка.dc.html lines 397-422:
|
||
// address + "В РАСЧЁТЕ" badge, four clickable per-section totals, and a
|
||
// "КАЧЕСТВО ДАННЫХ" block with a donut. Data from fixtures; clicks fire
|
||
// onNavigate(row.nav) only — no API, no fetching.
|
||
|
||
"use client";
|
||
|
||
import { tokens } from "./tokens";
|
||
import { object, summary } from "./fixtures";
|
||
import type { ObjectSummaryData } from "./mappers";
|
||
|
||
// Default presentation data (unwired usage): the existing design fixtures.
|
||
const OBJECT_SUMMARY_FIXTURE: ObjectSummaryData = { object, summary };
|
||
|
||
interface ObjectSummaryProps {
|
||
data?: ObjectSummaryData;
|
||
onNavigate: (i: number) => void;
|
||
}
|
||
|
||
// Audit M11 — name the unnamed «N · XX,XX млн» numbers. Each summary row value
|
||
// is built in mappers.mapSummary as "count · metric"; the metric is different
|
||
// per row. This caption labels each ·-separated token token-for-token. Keyed by
|
||
// label content so it matches both the mapper labels ("Продажи рядом") and the
|
||
// older fixture labels ("Продажи в доме"). Sources in mappers.mapSummary:
|
||
// Продажи рядом : houseSold · kpi.median_exposure_days
|
||
// Аналоги : e.n_analogs · e.median_price_rub (H3 — SAME field as the
|
||
// 02-РЕЗУЛЬТАТ hero «медиана объявлений», not the raw analog
|
||
// listing-price median which read far higher over the same set)
|
||
// Сделки : dealCount · streetDeals.median_price_rub ?? median(deals)
|
||
// Аналитика : kpi.median_exposure_days · -kpi.median_bargain_pct
|
||
function rowHint(label: string): string {
|
||
if (label.startsWith("Продажи")) return "кол-во · медиана экспозиции";
|
||
if (label === "Аналоги") return "кол-во · медиана объявлений";
|
||
if (label === "Сделки") return "кол-во · медиана сделок";
|
||
if (label === "Аналитика") return "экспозиция · торг";
|
||
return "";
|
||
}
|
||
|
||
export function ObjectSummary({
|
||
data = OBJECT_SUMMARY_FIXTURE,
|
||
onNavigate,
|
||
}: ObjectSummaryProps) {
|
||
return (
|
||
<div
|
||
style={{
|
||
position: "relative",
|
||
background: tokens.surface.w50,
|
||
backdropFilter: "blur(6px)",
|
||
border: `1px solid ${tokens.line2}`,
|
||
borderRadius: 8,
|
||
padding: "15px 16px",
|
||
flex: 1,
|
||
display: "flex",
|
||
flexDirection: "column",
|
||
color: tokens.ink,
|
||
fontFamily: tokens.font.sans,
|
||
}}
|
||
>
|
||
<style>{`
|
||
.os-row { background: none; border: none; transition: background .12s; }
|
||
.os-row:hover { background: rgba(46,139,255,.07); }
|
||
`}</style>
|
||
|
||
{/* HUD corner bracket */}
|
||
<div
|
||
style={{
|
||
position: "absolute",
|
||
right: -1,
|
||
top: -1,
|
||
width: 12,
|
||
height: 12,
|
||
borderRight: `1.5px solid ${tokens.bracket}`,
|
||
borderTop: `1.5px solid ${tokens.bracket}`,
|
||
}}
|
||
/>
|
||
|
||
{/* Header: 03 СВОДКА ОБЪЕКТА + В РАСЧЁТЕ badge */}
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
alignItems: "center",
|
||
justifyContent: "space-between",
|
||
marginBottom: 5,
|
||
}}
|
||
>
|
||
<div style={{ display: "flex", alignItems: "baseline", gap: 9 }}>
|
||
<span
|
||
style={{
|
||
fontFamily: tokens.font.mono,
|
||
fontSize: 14,
|
||
color: tokens.accent,
|
||
}}
|
||
>
|
||
03
|
||
</span>
|
||
{/* Panel heading — real <h2> (preflight not loaded → reset UA margin
|
||
inline; visuals unchanged). */}
|
||
<h2
|
||
style={{
|
||
margin: 0,
|
||
fontSize: 12,
|
||
fontWeight: 600,
|
||
letterSpacing: 1.5,
|
||
}}
|
||
>
|
||
СВОДКА ОБЪЕКТА
|
||
</h2>
|
||
</div>
|
||
<span
|
||
style={{
|
||
display: "flex",
|
||
alignItems: "center",
|
||
gap: 4,
|
||
fontSize: 8,
|
||
letterSpacing: 0.8,
|
||
color: tokens.success,
|
||
border: `1px solid ${tokens.successBorder}`,
|
||
borderRadius: 20,
|
||
padding: "2px 7px",
|
||
whiteSpace: "nowrap",
|
||
}}
|
||
>
|
||
<span
|
||
style={{
|
||
width: 5,
|
||
height: 5,
|
||
borderRadius: "50%",
|
||
background: tokens.success,
|
||
}}
|
||
/>
|
||
АКТУАЛЬНО
|
||
</span>
|
||
</div>
|
||
|
||
<div
|
||
style={{
|
||
fontSize: 9,
|
||
letterSpacing: 1.2,
|
||
color: tokens.body2,
|
||
marginBottom: 13,
|
||
}}
|
||
>
|
||
ИТОГИ ПО РАЗДЕЛАМ
|
||
</div>
|
||
|
||
{/* Address */}
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
alignItems: "flex-start",
|
||
gap: 9,
|
||
marginBottom: 14,
|
||
}}
|
||
>
|
||
<svg
|
||
width="15"
|
||
height="18"
|
||
viewBox="0 0 15 18"
|
||
fill="none"
|
||
style={{ marginTop: 1 }}
|
||
aria-hidden="true"
|
||
>
|
||
<path
|
||
d="M7.5 17C7.5 17 14 11 14 6.5A6.5 6.5 0 1 0 1 6.5C1 11 7.5 17 7.5 17Z"
|
||
stroke={tokens.accent}
|
||
strokeWidth="1.2"
|
||
/>
|
||
<circle cx="7.5" cy="6.5" r="2.3" fill={tokens.accent} />
|
||
</svg>
|
||
<div style={{ flex: 1 }}>
|
||
<div style={{ fontSize: 13, fontWeight: 600 }}>
|
||
{data.object.address}
|
||
</div>
|
||
<div style={{ fontSize: 11, color: tokens.muted, marginTop: 2 }}>
|
||
{data.object.city}
|
||
</div>
|
||
</div>
|
||
<svg width="13" height="13" viewBox="0 0 13 13" fill="none" aria-hidden="true">
|
||
<path
|
||
d="M5 1h7v7M12 1L6 7M9 8v4H1V4h4"
|
||
stroke={tokens.muted2}
|
||
strokeWidth="1"
|
||
/>
|
||
</svg>
|
||
</div>
|
||
|
||
{/* Per-section totals (clickable) */}
|
||
<div style={{ display: "flex", flexDirection: "column", gap: 0 }}>
|
||
{data.summary.rows
|
||
.filter((row) => row.value !== "—")
|
||
.map((row, i, visible) => (
|
||
<button
|
||
key={row.label}
|
||
type="button"
|
||
className="os-row"
|
||
onClick={() => onNavigate(row.nav)}
|
||
aria-label={`${row.label} — подробнее`}
|
||
style={{
|
||
display: "flex",
|
||
justifyContent: "space-between",
|
||
alignItems: "center",
|
||
padding: "9px 6px",
|
||
cursor: "pointer",
|
||
borderRadius: 5,
|
||
width: "100%",
|
||
fontFamily: "inherit",
|
||
textAlign: "left",
|
||
borderBottom:
|
||
i < visible.length - 1
|
||
? `1px dotted ${tokens.lineGradient}`
|
||
: undefined,
|
||
}}
|
||
>
|
||
<span
|
||
style={{
|
||
fontSize: 10.5,
|
||
color: tokens.body,
|
||
display: "flex",
|
||
alignItems: "center",
|
||
gap: 6,
|
||
whiteSpace: "nowrap",
|
||
paddingRight: 12,
|
||
}}
|
||
>
|
||
<span
|
||
style={{
|
||
width: 5,
|
||
height: 5,
|
||
borderRadius: "50%",
|
||
background:
|
||
row.dot === "accent" ? tokens.accent : tokens.scatterDeal,
|
||
}}
|
||
/>
|
||
{row.label}
|
||
</span>
|
||
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||
<span
|
||
style={{
|
||
display: "flex",
|
||
flexDirection: "column",
|
||
alignItems: "flex-end",
|
||
gap: 1,
|
||
}}
|
||
>
|
||
<span
|
||
style={{
|
||
fontFamily: tokens.font.mono,
|
||
fontSize: 11,
|
||
color: tokens.ink2,
|
||
whiteSpace: "nowrap",
|
||
}}
|
||
>
|
||
{row.value}
|
||
</span>
|
||
{rowHint(row.label) && (
|
||
<span
|
||
style={{
|
||
fontSize: 8,
|
||
letterSpacing: 0.2,
|
||
color: tokens.muted3,
|
||
whiteSpace: "nowrap",
|
||
}}
|
||
>
|
||
{rowHint(row.label)}
|
||
</span>
|
||
)}
|
||
</span>
|
||
<span style={{ color: tokens.accent, fontSize: 11 }}>›</span>
|
||
</span>
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{/* КАЧЕСТВО ДАННЫХ */}
|
||
<div
|
||
style={{
|
||
marginTop: 14,
|
||
background: tokens.infoSoftBg,
|
||
border: `1px solid ${tokens.infoSoftBorder}`,
|
||
borderRadius: 6,
|
||
padding: 12,
|
||
display: "flex",
|
||
gap: 12,
|
||
alignItems: "center",
|
||
}}
|
||
>
|
||
<svg width="50" height="50" viewBox="0 0 50 50" fill="none" aria-hidden="true">
|
||
<circle
|
||
cx="25"
|
||
cy="25"
|
||
r="20"
|
||
stroke={tokens.track}
|
||
strokeWidth="3"
|
||
/>
|
||
<circle
|
||
cx="25"
|
||
cy="25"
|
||
r="20"
|
||
stroke={tokens.accent}
|
||
strokeWidth="3"
|
||
strokeLinecap="round"
|
||
strokeDasharray="126"
|
||
strokeDashoffset="72"
|
||
transform="rotate(-90 25 25)"
|
||
/>
|
||
<circle cx="25" cy="25" r="11" stroke="#cdddec" strokeWidth="1" />
|
||
<circle cx="25" cy="25" r="1.6" fill={tokens.accent} />
|
||
</svg>
|
||
<div style={{ flex: 1 }}>
|
||
<div
|
||
style={{
|
||
fontSize: 9.5,
|
||
letterSpacing: 1.5,
|
||
color: tokens.muted2,
|
||
marginBottom: 7,
|
||
}}
|
||
>
|
||
КАЧЕСТВО ДАННЫХ
|
||
</div>
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
justifyContent: "space-between",
|
||
fontSize: 10,
|
||
color: tokens.muted,
|
||
lineHeight: 1.7,
|
||
}}
|
||
>
|
||
<span>Источников</span>
|
||
<span style={{ fontFamily: tokens.font.mono, color: tokens.ink }}>
|
||
{data.summary.quality.sources}
|
||
</span>
|
||
</div>
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
justifyContent: "space-between",
|
||
fontSize: 10,
|
||
color: tokens.muted,
|
||
lineHeight: 1.7,
|
||
}}
|
||
>
|
||
<span>Достоверность</span>
|
||
<span style={{ color: tokens.ink }}>
|
||
{data.summary.quality.confidence}
|
||
</span>
|
||
</div>
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
justifyContent: "space-between",
|
||
fontSize: 10,
|
||
color: tokens.muted,
|
||
lineHeight: 1.7,
|
||
}}
|
||
>
|
||
<span>Разброс цен</span>
|
||
<span
|
||
style={{ fontFamily: tokens.font.mono, color: tokens.accent }}
|
||
>
|
||
{data.summary.quality.cv}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|