From d63122d3e4c246d948f9029c01d3faea6eb84a26 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Thu, 2 Jul 2026 19:16:37 +0300 Subject: [PATCH] =?UTF-8?q?fix(tradein/v2):=20=D1=87=D0=B5=D1=81=D1=82?= =?UTF-8?q?=D0=BD=D0=B0=D1=8F=20=D0=BF=D0=BE=D0=B4=D0=B0=D1=87=D0=B0=20?= =?UTF-8?q?=D0=B8=D0=BD=D1=82=D0=B5=D1=80=D0=B2=D0=B0=D0=BB=D0=B0=20=D0=BE?= =?UTF-8?q?=D1=86=D0=B5=D0=BD=D0=BA=D0=B8=20=E2=80=94=20=D0=BE=D0=B4=D0=BD?= =?UTF-8?q?=D0=B0=20=D1=86=D0=B8=D1=84=D1=80=D0=B0-=D0=B3=D0=B5=D1=80?= =?UTF-8?q?=D0=BE=D0=B9=20+=20=D1=87=D0=B5=D1=81=D1=82=D0=BD=D1=8B=D0=B9?= =?UTF-8?q?=20=D1=80=D0=B0=D0=B7=D0=B1=D1=80=D0=BE=D1=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Оценщик жаловался на «большой интервал» между рекомендованной ценой и оценкой. Бэктест: ширину коридора косметически не сузить (реальная рыночная дисперсия + asking→sold gap). Чиним подачу, не число (математика не тронута). Витрина /trade-in/v2 (активная, wired на real data через mapResultPanel): - deltaLabel «К РЫНКУ» → «К ОБЪЯВЛ.» (mappers.ts + fixtures.ts): −N% — разрыв «объявление → ожидаемая сделка», а не «скидка от нас». - ResultPanel: честная подпись под ценовыми карточками — «Диапазоны показывают разброс цен на рынке, а не погрешность оценки». Широкий диапазон не читается как неуверенность расчёта. - Иерархия expected_sold=headline / median=baseline / ДКП=справочно уже была (audit H2, #2062/#2081) — не трогаем. Легаси-витрина / (HeroSummary), тот же принцип и устранение конфликта термина: - hero-duo: expected_sold — цифра-герой («Оценка · ожидаемая цена продажи», 34px, accent), median — вторичный «Рекомендованная цена в объявлении» (20px, приглушённый) вместо двух равнозначных 28px-блоков «Выставить»/ «Получить». Новый модификатор .hero-duo--sold-primary. - Диапазон sold подписан «рыночный разброс» + honest hint «Диапазон отражает разброс цен по рынку, а не погрешность оценки». - Бейдж «−N%» → «−N% к объявлению» + title (разрыв asking↔сделка, не скидка). - OfferCard: «Оценка по рынку» → «Цена в объявлении» (median ≠ «оценка»; слово «оценка» на витрине = expected_sold). Проверка: tsc --noEmit чисто; next build чисто (lint+types, 13 роутов). --- .../src/components/trade-in/HeroSummary.tsx | 109 +++++++++++------- .../src/components/trade-in/OfferCard.tsx | 2 +- .../src/components/trade-in/trade-in.css | 30 +++++ .../components/trade-in/v2/ResultPanel.tsx | 14 +++ .../src/components/trade-in/v2/fixtures.ts | 2 +- .../src/components/trade-in/v2/mappers.ts | 4 +- 6 files changed, 114 insertions(+), 47 deletions(-) diff --git a/tradein-mvp/frontend/src/components/trade-in/HeroSummary.tsx b/tradein-mvp/frontend/src/components/trade-in/HeroSummary.tsx index 1b5fa8c0..f0fc7b62 100644 --- a/tradein-mvp/frontend/src/components/trade-in/HeroSummary.tsx +++ b/tradein-mvp/frontend/src/components/trade-in/HeroSummary.tsx @@ -390,10 +390,18 @@ export function HeroSummary({ estimate, input, onResubmit, isResubmitting = fals )} - {/* ── Две равноправные hero-цены: «Выставить» (asking) + «Получить» (sold). ── - При hasSold=false (старые оценки / пустая ratio) — одиночный layout - только с «Выставить». Empty-state при insufficientData. */} -
+ {/* ── §1.1 «Одна цифра + честный разброс»: expected_sold — цифра-герой + («оценка» / ожидаемая цена продажи), median — вторичный ориентир + «рекомендованная цена в объявлении», НЕ конкурирует за внимание. + При hasSold=false (старые оценки / пустая ratio) median остаётся + единственной честной цифрой. Empty-state при insufficientData. */} +
{insufficientData ? (
Оценка недоступна
@@ -404,11 +412,50 @@ export function HeroSummary({ estimate, input, onResubmit, isResubmitting = fals снова.

- ) : ( + ) : hasSold ? ( <> - {/* Левая ячейка: «Выставить в объявлении» */} + {/* Цифра-герой: «оценка» = ожидаемая цена продажи (expected_sold) */} +
+
+ Оценка · ожидаемая цена продажи + {showDiscount && ( + + −{discountPct}% к объявлению + + )} +
+
{formatMln(sold as number)} ₽
+ {typeof soldLo === "number" && typeof soldHi === "number" && ( +
+ рыночный разброс: {formatMln(soldLo)} – {formatMln(soldHi)} ₽ +
+ )} + {typeof soldPerM2 === "number" && ( +
+ {soldPerM2.toLocaleString("ru-RU")} ₽/м² +
+ )} +

+ Диапазон отражает разброс цен по рынку, а не погрешность оценки. +

+ {estimate.ratio_basis === "global_fallback" ? ( +
+ Ср.-городской коэффициент (мало сделок в сегменте) +
+ ) : estimate.ratio_basis === "per_rooms" ? ( +
+ Коэффициент по сделкам той же планировки +
+ ) : null} +
+ + {/* Вторичный ориентир: рекомендованная цена в объявлении (median) */}
-
Выставить в объявлении
+
Рекомендованная цена в объявлении
{formatMln(m)} ₽
{formatMln(lo)} – {formatMln(hi)} ₽ @@ -417,44 +464,18 @@ export function HeroSummary({ estimate, input, onResubmit, isResubmitting = fals {estimate.median_price_per_m2.toLocaleString("ru-RU")} ₽/м²
- - {/* Правая ячейка: «Получить (ожидаемая сделка)» — только при hasSold */} - {hasSold && ( -
-
- Получить (ожидаемая сделка) - {showDiscount && ( - - −{discountPct}% - - )} -
-
{formatMln(sold as number)} ₽
- {typeof soldLo === "number" && typeof soldHi === "number" && ( -
- {formatMln(soldLo)} – {formatMln(soldHi)} ₽ -
- )} - {typeof soldPerM2 === "number" && ( -
- {soldPerM2.toLocaleString("ru-RU")} ₽/м² -
- )} - {estimate.ratio_basis === "global_fallback" ? ( -
- Ср.-городской коэффициент (мало сделок в сегменте) -
- ) : estimate.ratio_basis === "per_rooms" ? ( -
- Коэффициент по сделкам той же планировки -
- ) : null} -
- )} + ) : ( +
+
Рекомендованная цена в объявлении
+
{formatMln(m)} ₽
+
+ {formatMln(lo)} – {formatMln(hi)} ₽ +
+
+ {estimate.median_price_per_m2.toLocaleString("ru-RU")} ₽/м² +
+
)}
diff --git a/tradein-mvp/frontend/src/components/trade-in/OfferCard.tsx b/tradein-mvp/frontend/src/components/trade-in/OfferCard.tsx index 57afae91..0bc96998 100644 --- a/tradein-mvp/frontend/src/components/trade-in/OfferCard.tsx +++ b/tradein-mvp/frontend/src/components/trade-in/OfferCard.tsx @@ -58,7 +58,7 @@ export function OfferCard({ estimate, brandSlug }: Props) {
- Оценка по рынку:{" "} + Цена в объявлении:{" "} {estimate.insufficient_data ? "—" : `${formatMln(estimate.median_price_rub)} ₽`} diff --git a/tradein-mvp/frontend/src/components/trade-in/trade-in.css b/tradein-mvp/frontend/src/components/trade-in/trade-in.css index 36bfb28f..9af42350 100644 --- a/tradein-mvp/frontend/src/components/trade-in/trade-in.css +++ b/tradein-mvp/frontend/src/components/trade-in/trade-in.css @@ -756,6 +756,35 @@ font-style: italic; } + /* ── §1.1 «Одна цифра-герой»: expected_sold доминирует, median — вторичный + ориентир (не конкурирует за внимание). Применяется только когда есть + expected_sold (класс .hero-duo--sold-primary). ── */ + .hero-duo--sold-primary { + grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr); + align-items: stretch; + } + /* Цифра-герой (expected_sold) — крупнее, чем вторичный median. */ + .hero-duo--sold-primary .hero-duo__cell[data-role=sold] .hero-duo__value { + font-size: 34px; + } + /* Вторичный median-ориентир — приглушённый фон + меньший кегль числа. */ + .hero-duo--sold-primary .hero-duo__cell[data-role=asking] { + background: var(--surface-1, var(--bg)); + } + .hero-duo--sold-primary .hero-duo__cell[data-role=asking] .hero-duo__value { + font-size: 20px; + font-weight: 600; + color: var(--fg-2); + } + /* Честное пояснение к разбросу — «не погрешность оценки». */ + .hero-duo__hint { + margin: 6px 0 0; + font-size: 11px; + line-height: 1.45; + color: var(--muted); + max-width: 44ch; + } + /* ── Компактный ряд бенчмарков под hero-duo ── */ .hero-benchmarks { display: flex; @@ -1405,6 +1434,7 @@ .form-card { position: static; max-height: none; } .hero-top { grid-template-columns: 1fr; } .hero-duo { grid-template-columns: 1fr; } + .hero-duo--sold-primary { grid-template-columns: 1fr; } .hero-prices { grid-template-columns: 1fr; } .hero-photo { aspect-ratio: 16/9; } .progress-list { grid-template-columns: 1fr; } diff --git a/tradein-mvp/frontend/src/components/trade-in/v2/ResultPanel.tsx b/tradein-mvp/frontend/src/components/trade-in/v2/ResultPanel.tsx index be303949..47b04928 100644 --- a/tradein-mvp/frontend/src/components/trade-in/v2/ResultPanel.tsx +++ b/tradein-mvp/frontend/src/components/trade-in/v2/ResultPanel.tsx @@ -432,6 +432,20 @@ export default function ResultPanel({ })}
+ {/* §1.2 честный разброс: диапазоны цен = рыночный спред, НЕ погрешность + оценки. Ставим сразу под ценовыми карточками, чтобы широкий диапазон + не читался как «неуверенность расчёта». */} +
+ Диапазоны показывают разброс цен на рынке, а не погрешность оценки. +
+ {/* ranges + radar */}