Merge pull request 'fix(tradein/v2): честная подача интервала оценки — одна цифра-герой + честный разброс' (#2159) from fix/tradein-v2-hero-honest-interval into main
Some checks failed
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Failing after 1m31s
Deploy Trade-In / build-frontend (push) Successful in 2m14s
Deploy Trade-In / build-backend (push) Has been skipped
Deploy Trade-In / deploy (push) Has been skipped
Some checks failed
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Failing after 1m31s
Deploy Trade-In / build-frontend (push) Successful in 2m14s
Deploy Trade-In / build-backend (push) Has been skipped
Deploy Trade-In / deploy (push) Has been skipped
This commit is contained in:
commit
86a22dfc8f
6 changed files with 114 additions and 47 deletions
|
|
@ -390,10 +390,18 @@ export function HeroSummary({ estimate, input, onResubmit, isResubmitting = fals
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Две равноправные hero-цены: «Выставить» (asking) + «Получить» (sold). ──
|
||||
При hasSold=false (старые оценки / пустая ratio) — одиночный layout
|
||||
только с «Выставить». Empty-state при insufficientData. */}
|
||||
<div className={`hero-duo${hasSold && !insufficientData ? "" : " hero-duo--single"}`}>
|
||||
{/* ── §1.1 «Одна цифра + честный разброс»: expected_sold — цифра-герой
|
||||
(«оценка» / ожидаемая цена продажи), median — вторичный ориентир
|
||||
«рекомендованная цена в объявлении», НЕ конкурирует за внимание.
|
||||
При hasSold=false (старые оценки / пустая ratio) median остаётся
|
||||
единственной честной цифрой. Empty-state при insufficientData. */}
|
||||
<div
|
||||
className={
|
||||
!insufficientData && hasSold
|
||||
? "hero-duo hero-duo--sold-primary"
|
||||
: "hero-duo hero-duo--single"
|
||||
}
|
||||
>
|
||||
{insufficientData ? (
|
||||
<div className="hero-duo__cell">
|
||||
<div className="hero-duo__label">Оценка недоступна</div>
|
||||
|
|
@ -404,11 +412,50 @@ export function HeroSummary({ estimate, input, onResubmit, isResubmitting = fals
|
|||
снова.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
) : hasSold ? (
|
||||
<>
|
||||
{/* Левая ячейка: «Выставить в объявлении» */}
|
||||
{/* Цифра-герой: «оценка» = ожидаемая цена продажи (expected_sold) */}
|
||||
<div className="hero-duo__cell" data-role="sold">
|
||||
<div className="hero-duo__label">
|
||||
Оценка · ожидаемая цена продажи
|
||||
{showDiscount && (
|
||||
<span
|
||||
className="hero-duo__delta"
|
||||
title="Насколько ожидаемая цена сделки ниже цены в объявлении. Это разрыв «объявление → реальная сделка», а не скидка сервиса."
|
||||
aria-label={`ожидаемая цена продажи на ${discountPct}% ниже цены в объявлении`}
|
||||
>
|
||||
−{discountPct}% к объявлению
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="hero-duo__value mono">{formatMln(sold as number)} ₽</div>
|
||||
{typeof soldLo === "number" && typeof soldHi === "number" && (
|
||||
<div className="hero-duo__range mono">
|
||||
рыночный разброс: {formatMln(soldLo)} – {formatMln(soldHi)} ₽
|
||||
</div>
|
||||
)}
|
||||
{typeof soldPerM2 === "number" && (
|
||||
<div className="hero-duo__perm2 mono">
|
||||
{soldPerM2.toLocaleString("ru-RU")} ₽/м²
|
||||
</div>
|
||||
)}
|
||||
<p className="hero-duo__hint">
|
||||
Диапазон отражает разброс цен по рынку, а не погрешность оценки.
|
||||
</p>
|
||||
{estimate.ratio_basis === "global_fallback" ? (
|
||||
<div className="hero-duo__note">
|
||||
Ср.-городской коэффициент (мало сделок в сегменте)
|
||||
</div>
|
||||
) : estimate.ratio_basis === "per_rooms" ? (
|
||||
<div className="hero-duo__note">
|
||||
Коэффициент по сделкам той же планировки
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* Вторичный ориентир: рекомендованная цена в объявлении (median) */}
|
||||
<div className="hero-duo__cell" data-role="asking">
|
||||
<div className="hero-duo__label">Выставить в объявлении</div>
|
||||
<div className="hero-duo__label">Рекомендованная цена в объявлении</div>
|
||||
<div className="hero-duo__value mono">{formatMln(m)} ₽</div>
|
||||
<div className="hero-duo__range mono">
|
||||
{formatMln(lo)} – {formatMln(hi)} ₽
|
||||
|
|
@ -417,44 +464,18 @@ export function HeroSummary({ estimate, input, onResubmit, isResubmitting = fals
|
|||
{estimate.median_price_per_m2.toLocaleString("ru-RU")} ₽/м²
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Правая ячейка: «Получить (ожидаемая сделка)» — только при hasSold */}
|
||||
{hasSold && (
|
||||
<div className="hero-duo__cell" data-role="sold">
|
||||
<div className="hero-duo__label">
|
||||
Получить (ожидаемая сделка)
|
||||
{showDiscount && (
|
||||
<span
|
||||
className="hero-duo__delta"
|
||||
aria-label={`на ${discountPct}% ниже цены в объявлении`}
|
||||
>
|
||||
−{discountPct}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="hero-duo__value mono">{formatMln(sold as number)} ₽</div>
|
||||
{typeof soldLo === "number" && typeof soldHi === "number" && (
|
||||
<div className="hero-duo__range mono">
|
||||
{formatMln(soldLo)} – {formatMln(soldHi)} ₽
|
||||
</div>
|
||||
)}
|
||||
{typeof soldPerM2 === "number" && (
|
||||
<div className="hero-duo__perm2 mono">
|
||||
{soldPerM2.toLocaleString("ru-RU")} ₽/м²
|
||||
</div>
|
||||
)}
|
||||
{estimate.ratio_basis === "global_fallback" ? (
|
||||
<div className="hero-duo__note">
|
||||
Ср.-городской коэффициент (мало сделок в сегменте)
|
||||
</div>
|
||||
) : estimate.ratio_basis === "per_rooms" ? (
|
||||
<div className="hero-duo__note">
|
||||
Коэффициент по сделкам той же планировки
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="hero-duo__cell" data-role="asking">
|
||||
<div className="hero-duo__label">Рекомендованная цена в объявлении</div>
|
||||
<div className="hero-duo__value mono">{formatMln(m)} ₽</div>
|
||||
<div className="hero-duo__range mono">
|
||||
{formatMln(lo)} – {formatMln(hi)} ₽
|
||||
</div>
|
||||
<div className="hero-duo__perm2 mono">
|
||||
{estimate.median_price_per_m2.toLocaleString("ru-RU")} ₽/м²
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ export function OfferCard({ estimate, brandSlug }: Props) {
|
|||
</div>
|
||||
<div className="card-meta">
|
||||
<div>
|
||||
Оценка по рынку:{" "}
|
||||
Цена в объявлении:{" "}
|
||||
<b className="mono">
|
||||
{estimate.insufficient_data ? "—" : `${formatMln(estimate.median_price_rub)} ₽`}
|
||||
</b>
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -432,6 +432,20 @@ export default function ResultPanel({
|
|||
})}
|
||||
</div>
|
||||
|
||||
{/* §1.2 честный разброс: диапазоны цен = рыночный спред, НЕ погрешность
|
||||
оценки. Ставим сразу под ценовыми карточками, чтобы широкий диапазон
|
||||
не читался как «неуверенность расчёта». */}
|
||||
<div
|
||||
style={{
|
||||
fontSize: 10,
|
||||
lineHeight: 1.5,
|
||||
color: hint,
|
||||
marginTop: -4,
|
||||
}}
|
||||
>
|
||||
Диапазоны показывают разброс цен на рынке, а не погрешность оценки.
|
||||
</div>
|
||||
|
||||
{/* ranges + radar */}
|
||||
<div
|
||||
style={{
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ export const resultCards: ResultCard[] = [
|
|||
range: "6,83 — 12,81 млн ₽",
|
||||
ppm: "199 973 ₽/м²",
|
||||
delta: "−11%",
|
||||
deltaLabel: "К РЫНКУ",
|
||||
deltaLabel: "К ОБЪЯВЛ.",
|
||||
nav: 2,
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -855,7 +855,9 @@ export function mapResultPanel(
|
|||
e.asking_to_sold_ratio != null
|
||||
? fmtPct((e.asking_to_sold_ratio - 1) * 100)
|
||||
: "—",
|
||||
deltaLabel: "К РЫНКУ",
|
||||
// §1.3: разрыв «цена в объявлении → ожидаемая сделка», НЕ скидка сервиса.
|
||||
// «К ОБЪЯВЛ.» однозначнее прежнего «К РЫНКУ» (не читается как «скидка от нас»).
|
||||
deltaLabel: "К ОБЪЯВЛ.",
|
||||
note:
|
||||
dealTier != null &&
|
||||
e.expected_sold_price_rub != null &&
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue