gendesign/tradein-mvp/frontend/src/components/trade-in/v2/ResultPanel.tsx
bot-backend 267e5dfc5b
All checks were successful
CI / changes (pull_request) Successful in 7s
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
fix(tradein/v2): убрать средний скаттер «ЦЕНА × СРОК ПРОДАЖИ» из результата
По фидбэку: куцый inline-скаттер в 02 РЕЗУЛЬТАТ мало что говорил и дублировал
полный price×time график в Аналитике. Удалён; ряд диапазонов теперь grid 1fr 1fr —
два range-бара (объявления/сделки) на всю ширину, читаются прямее. Полный
точечный график остаётся в оверлее Аналитики (таб 06).

- ResultPanel.tsx: удалён scatter-блок (title + SVG + легенда + «Подробнее»),
  grid ряда «1fr 168px 1fr» -> «1fr 1fr»; убраны ставшие мёртвыми токены
  scatterDeal / line3.
- data.scatterMini в контракте оставлен (маппер всё ещё считает, просто не
  потребляется) — чистку контракта можно сделать отдельно.

tsc + next build green.
2026-06-29 02:20:45 +03:00

785 lines
22 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";
import { Fragment } from "react";
import { tokens } from "./tokens";
import {
ranges,
resultCards,
resultMeta,
scatterMini,
sources,
} from "./fixtures";
import type { ResultPanelData } from "./mappers";
const {
accent,
ink,
body2,
muted,
muted2,
muted3,
line,
line2,
lineSoft,
bracket,
barLo,
barMid,
track,
success,
hint,
disabledBorder,
disabledValue,
disabledLabel,
infoSoftBg,
surface,
font,
} = tokens;
// Default presentation data (unwired usage): the existing design fixtures.
const RESULT_FIXTURE: ResultPanelData = {
cards: resultCards,
meta: resultMeta,
ranges,
scatterMini,
sources,
};
interface ResultPanelProps {
data?: ResultPanelData;
onNavigate: (i: number) => void;
}
// Mini-histogram column tint: peak column = accent, its neighbours = mid, rest = lo.
function barColor(i: number, bars: number[]): string {
const peak = bars.indexOf(Math.max(...bars));
if (i === peak) return accent;
if (i === peak - 1 || i === peak + 1) return barMid;
return barLo;
}
// Render a string[] with <br/> between lines (faithful to the design line breaks).
function lines(parts: string[]) {
return parts.map((part, i) => (
<Fragment key={i}>
{i > 0 && <br />}
{part}
</Fragment>
));
}
// Split a source name on its parenthetical so the design's forced
// "РОСРЕЕСТР / (ВНУТР.)" two-line break (line-height 1.3) is reproduced; plain
// names without " (" stay on a single line.
function sourceName(name: string) {
const idx = name.indexOf(" (");
if (idx === -1) return name;
return (
<>
{name.slice(0, idx)}
<br />
{name.slice(idx + 1)}
</>
);
}
export default function ResultPanel({
data = RESULT_FIXTURE,
onNavigate,
}: ResultPanelProps) {
return (
<div
style={{ display: "flex", flexDirection: "column", gap: 14, minWidth: 0 }}
>
<style>{`
.rp-more { transition: opacity 120ms ease; }
.rp-more:hover { opacity: .6; }
`}</style>
{/* header */}
<div
style={{
display: "flex",
alignItems: "baseline",
justifyContent: "space-between",
}}
>
<div style={{ display: "flex", alignItems: "baseline", gap: 10 }}>
<span style={{ fontFamily: font.mono, fontSize: 15, color: accent }}>
02
</span>
<span style={{ fontSize: 13, fontWeight: 600, letterSpacing: 2 }}>
РЕЗУЛЬТАТ ОЦЕНКИ
</span>
</div>
<div
style={{
display: "flex",
alignItems: "center",
gap: 22,
fontSize: 10,
letterSpacing: 1,
color: muted,
}}
>
<span>
ИСТОЧНИКОВ:{" "}
<b style={{ color: ink, fontFamily: font.mono }}>
{data.meta.sources}
</b>
</span>
<span>
ДОСТОВЕРНОСТЬ: <b style={{ color: ink }}>{data.meta.confidence}</b>
</span>
<span>
CV{" "}
<b style={{ color: accent, fontFamily: font.mono }}>
{data.meta.cv}
</b>
</span>
</div>
</div>
{/* result cards */}
<div
style={{
display: "grid",
gridTemplateColumns: "1fr 1fr 1fr",
gap: 14,
flex: "0 0 auto",
}}
>
{data.cards.map((card, ci) => {
// Price hierarchy (audit H2). The three cards are NOT equal weight:
// ci 0 — РЕКОМЕНДОВАННАЯ ЦЕНА В ОБЪЯВЛЕНИИ (market asking, baseline)
// ci 1 — ОЖИДАЕМАЯ ЦЕНА СДЕЛКИ (the real answer → headline)
// ci 2 — ДКП · РОСРЕЕСТР (registry reference, ~43% → demoted)
// The headline gets an accent border + tint + glow + larger accent
// value so the user can't confuse it with the registry figure; the
// registry card is demoted to a softer, smaller, muted "СПРАВОЧНО"
// tile. All three values stay (honesty); wiring (mapResultPanel) is
// untouched — emphasis is presentation only.
const isHeadline = ci === 1;
const isRegistry = ci === 2;
return (
<div
key={ci}
style={{
position: "relative",
background: isHeadline
? infoSoftBg
: isRegistry
? surface.w40
: surface.w55,
backdropFilter: "blur(6px)",
border: isHeadline
? `1.5px solid ${accent}`
: isRegistry
? `1px solid ${lineSoft}`
: `1px solid ${line2}`,
borderRadius: 8,
padding: "15px 16px 13px",
boxShadow: isHeadline
? "0 6px 22px rgba(46,139,255,.16)"
: undefined,
}}
>
<div
style={{
position: "absolute",
left: -1,
top: -1,
width: 12,
height: 12,
borderLeft: `1.5px solid ${isHeadline ? accent : bracket}`,
borderTop: `1.5px solid ${isHeadline ? accent : bracket}`,
}}
/>
<div
style={{
fontSize: 9.5,
letterSpacing: 1.5,
color: isHeadline ? accent : isRegistry ? muted2 : body2,
fontWeight: isHeadline ? 600 : undefined,
lineHeight: 1.6,
}}
>
{lines(card.title)}
</div>
{isRegistry && (
<div
style={{
fontSize: 8,
letterSpacing: 1.2,
color: muted3,
marginTop: 3,
}}
>
СПРАВОЧНО
</div>
)}
<div
style={{
marginTop: 14,
display: "flex",
alignItems: "baseline",
gap: 6,
}}
>
<span
style={{
fontFamily: font.mono,
fontSize: isHeadline ? 40 : isRegistry ? 28 : 38,
fontWeight: isHeadline ? 600 : 400,
letterSpacing: -1,
color: isHeadline ? accent : isRegistry ? muted : undefined,
}}
>
{card.value}
</span>
<span
style={{
fontSize: 13,
color: muted,
letterSpacing: 1,
whiteSpace: "nowrap",
}}
>
{card.unit}
</span>
</div>
<div
style={{
fontFamily: font.mono,
fontSize: 11,
color: muted,
marginTop: 10,
}}
>
{card.range}
</div>
<div
style={{
fontFamily: font.mono,
fontSize: 10,
color: muted2,
marginTop: 4,
}}
>
{card.ppm}
</div>
{card.note && (
<div
style={{
fontSize: 9,
color: muted2,
marginTop: 4,
lineHeight: 1.4,
maxWidth: "82%",
}}
>
{card.note}
</div>
)}
{card.bars ? (
<div
style={{
display: "flex",
alignItems: "flex-end",
justifyContent: "space-between",
gap: 12,
marginTop: 12,
}}
>
<div
style={{
display: "flex",
alignItems: "flex-end",
gap: 2,
height: 20,
flex: 1,
}}
>
{card.bars.map((h, bi) => (
<span
key={bi}
style={{
flex: 1,
height: `${h}%`,
background: barColor(bi, card.bars as number[]),
}}
/>
))}
</div>
<button
type="button"
className="rp-more"
onClick={() => onNavigate(card.nav)}
aria-label={`Подробнее — ${card.title.join(" ")}`}
style={{
display: "flex",
alignItems: "center",
gap: 4,
cursor: "pointer",
fontSize: 9,
letterSpacing: 0.3,
color: accent,
whiteSpace: "nowrap",
background: "none",
border: "none",
padding: 0,
margin: 0,
fontFamily: "inherit",
}}
>
Подробнее <span></span>
</button>
</div>
) : (
<>
<button
type="button"
className="rp-more"
onClick={() => onNavigate(card.nav)}
aria-label={`Подробнее — ${card.title.join(" ")}`}
style={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
gap: 4,
cursor: "pointer",
fontSize: 9,
letterSpacing: 0.3,
color: accent,
width: "100%",
background: "none",
border: "none",
padding: 0,
margin: 0,
marginTop: 16,
fontFamily: "inherit",
}}
>
Подробнее <span></span>
</button>
<div
style={{
position: "absolute",
right: 14,
top: 13,
width: 58,
height: 58,
}}
>
<svg width="58" height="58" viewBox="0 0 58 58">
<circle
cx="29"
cy="29"
r="25"
fill="none"
stroke={track}
strokeWidth="3"
/>
<circle
cx="29"
cy="29"
r="25"
fill="none"
stroke={accent}
strokeWidth="3"
strokeLinecap="round"
strokeDasharray="157"
strokeDashoffset="140"
transform="rotate(-90 29 29)"
/>
</svg>
<div
style={{
position: "absolute",
inset: 0,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
}}
>
<span
style={{
fontFamily: font.mono,
fontSize: 13,
fontWeight: 500,
color: accent,
}}
>
{card.delta}
</span>
<span
style={{
fontSize: 6.5,
letterSpacing: 1,
color: muted2,
marginTop: 1,
}}
>
{card.deltaLabel}
</span>
</div>
</div>
</>
)}
</div>
);
})}
</div>
{/* ranges + radar */}
<div
style={{
position: "relative",
background: surface.w50,
backdropFilter: "blur(6px)",
border: `1px solid ${line2}`,
borderRadius: 8,
padding: "18px 20px",
display: "grid",
gridTemplateColumns: "1fr 1fr",
gap: 24,
alignItems: "center",
flex: "0 0 auto",
}}
>
<div
style={{
position: "absolute",
right: -1,
bottom: -1,
width: 13,
height: 13,
borderRight: `1.5px solid ${bracket}`,
borderBottom: `1.5px solid ${bracket}`,
}}
/>
{/* left range — ads */}
<div>
<div
style={{
fontSize: 9,
letterSpacing: 1.3,
color: muted2,
lineHeight: 1.5,
}}
>
{lines(data.ranges.ads.label)}
</div>
<div
style={{
fontFamily: font.mono,
fontSize: 11,
color: muted,
margin: "12px 0 16px",
}}
>
медиана · <b style={{ color: ink }}>{data.ranges.ads.median}</b>
</div>
<div
style={{
position: "relative",
height: 3,
background: track,
borderRadius: 2,
}}
>
<div
style={{
position: "absolute",
left: data.ranges.ads.marker.fillLeft,
right: data.ranges.ads.marker.fillRight,
top: 0,
bottom: 0,
background: `linear-gradient(90deg,${barMid},${accent})`,
borderRadius: 2,
}}
/>
<div
style={{
position: "absolute",
left: data.ranges.ads.marker.dotLeft,
top: "50%",
transform: "translate(-50%,-50%)",
width: 3,
height: 16,
borderRadius: 1,
background: accent,
}}
/>
</div>
<div
style={{
display: "flex",
justifyContent: "space-between",
fontFamily: font.mono,
fontSize: 10,
color: muted2,
marginTop: 10,
}}
>
<span>{data.ranges.ads.lo}</span>
<span style={{ color: accent }}>{data.ranges.ads.median}</span>
<span>{data.ranges.ads.hi}</span>
</div>
</div>
{/* right range — deals */}
<div>
<div
style={{
fontSize: 9,
letterSpacing: 1.3,
color: muted2,
lineHeight: 1.5,
}}
>
{lines(data.ranges.deals.label)}
</div>
<div
style={{
fontFamily: font.mono,
fontSize: 11,
color: muted,
margin: "12px 0 16px",
}}
>
медиана · <b style={{ color: ink }}>{data.ranges.deals.median}</b>
</div>
<div
style={{
position: "relative",
height: 3,
background: track,
borderRadius: 2,
}}
>
<div
style={{
position: "absolute",
left: data.ranges.deals.marker.fillLeft,
right: data.ranges.deals.marker.fillRight,
top: 0,
bottom: 0,
background: `linear-gradient(90deg,${barMid},${accent})`,
borderRadius: 2,
}}
/>
<div
style={{
position: "absolute",
left: data.ranges.deals.marker.dotLeft,
top: "50%",
transform: "translate(-50%,-50%)",
width: 3,
height: 16,
borderRadius: 1,
background: accent,
}}
/>
</div>
<div
style={{
display: "flex",
justifyContent: "space-between",
fontFamily: font.mono,
fontSize: 10,
color: muted2,
marginTop: 10,
}}
>
<span>{data.ranges.deals.lo}</span>
<span style={{ color: accent }}>{data.ranges.deals.median}</span>
<span>{data.ranges.deals.hi}</span>
</div>
</div>
</div>
{/* sources */}
<div
style={{
background: surface.w50,
backdropFilter: "blur(6px)",
border: `1px solid ${line2}`,
borderRadius: 8,
padding: "14px 18px 13px",
flex: 1,
minHeight: 0,
display: "flex",
flexDirection: "column",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
marginBottom: 11,
}}
>
<span
style={{
fontSize: 11,
fontWeight: 600,
letterSpacing: 2,
color: ink,
}}
>
ИСТОЧНИКИ ДАННЫХ
</span>
<button
type="button"
className="rp-more"
onClick={() => onNavigate(2)}
aria-label="Подробнее — источники данных"
style={{
display: "flex",
alignItems: "center",
gap: 4,
cursor: "pointer",
fontSize: 9.5,
letterSpacing: 0.3,
color: accent,
background: "none",
border: "none",
padding: 0,
margin: 0,
fontFamily: "inherit",
}}
>
Подробнее <span></span>
</button>
</div>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(7,1fr)",
gap: 9,
flex: 1,
minHeight: 0,
alignItems: "stretch",
alignContent: "stretch",
}}
>
{data.sources.map((s, si) =>
s.active ? (
<div
key={si}
style={{
border: `1px solid ${line}`,
borderRadius: 6,
padding: "10px 9px",
display: "flex",
flexDirection: "column",
background: surface.w50,
}}
>
<div
style={{
fontSize: 9,
letterSpacing: 0.5,
color: ink,
fontWeight: 600,
...(s.name.includes(" (") ? { lineHeight: 1.3 } : {}),
}}
>
{sourceName(s.name)}
</div>
<div
style={{
fontFamily: font.mono,
fontSize: 26,
fontWeight: 300,
marginTop: "auto",
}}
>
{s.count}
</div>
<div
style={{
display: "flex",
alignItems: "center",
gap: 5,
fontSize: 9,
color: muted,
marginTop: 3,
}}
>
<span
style={{
width: 6,
height: 6,
borderRadius: "50%",
background: success,
}}
/>
{s.label}
</div>
</div>
) : (
<div
key={si}
style={{
border: `1px solid ${disabledBorder}`,
borderRadius: 6,
padding: "10px 9px",
display: "flex",
flexDirection: "column",
background: surface.w25,
}}
>
<div
style={{
fontSize: 9,
letterSpacing: 0.5,
color: muted3,
fontWeight: 600,
}}
>
{s.name}
</div>
<div
style={{
fontFamily: font.mono,
fontSize: 22,
fontWeight: 300,
marginTop: "auto",
color: disabledValue,
}}
>
{s.count}
</div>
<div
style={{ fontSize: 9, color: disabledLabel, marginTop: 3 }}
>
{s.label}
</div>
</div>
),
)}
</div>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginTop: 11,
}}
>
<span style={{ fontSize: 10, color: hint }}>
При недоступности источника частичный результат не блокируется
</span>
<span style={{ fontSize: 9.5, letterSpacing: 1.5, color: muted2 }}>
{data.meta.builtOn ?? "—"}
</span>
</div>
</div>
</div>
);
}