fix(tradein/v2): честная подача интервала оценки — одна цифра-герой + честный разброс #2159
6 changed files with 114 additions and 47 deletions
|
|
@ -390,10 +390,18 @@ export function HeroSummary({ estimate, input, onResubmit, isResubmitting = fals
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ── Две равноправные hero-цены: «Выставить» (asking) + «Получить» (sold). ──
|
{/* ── §1.1 «Одна цифра + честный разброс»: expected_sold — цифра-герой
|
||||||
При hasSold=false (старые оценки / пустая ratio) — одиночный layout
|
(«оценка» / ожидаемая цена продажи), median — вторичный ориентир
|
||||||
только с «Выставить». Empty-state при insufficientData. */}
|
«рекомендованная цена в объявлении», НЕ конкурирует за внимание.
|
||||||
<div className={`hero-duo${hasSold && !insufficientData ? "" : " hero-duo--single"}`}>
|
При hasSold=false (старые оценки / пустая ratio) median остаётся
|
||||||
|
единственной честной цифрой. Empty-state при insufficientData. */}
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
!insufficientData && hasSold
|
||||||
|
? "hero-duo hero-duo--sold-primary"
|
||||||
|
: "hero-duo hero-duo--single"
|
||||||
|
}
|
||||||
|
>
|
||||||
{insufficientData ? (
|
{insufficientData ? (
|
||||||
<div className="hero-duo__cell">
|
<div className="hero-duo__cell">
|
||||||
<div className="hero-duo__label">Оценка недоступна</div>
|
<div className="hero-duo__label">Оценка недоступна</div>
|
||||||
|
|
@ -404,11 +412,50 @@ export function HeroSummary({ estimate, input, onResubmit, isResubmitting = fals
|
||||||
снова.
|
снова.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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__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__value mono">{formatMln(m)} ₽</div>
|
||||||
<div className="hero-duo__range mono">
|
<div className="hero-duo__range mono">
|
||||||
{formatMln(lo)} – {formatMln(hi)} ₽
|
{formatMln(lo)} – {formatMln(hi)} ₽
|
||||||
|
|
@ -417,44 +464,18 @@ export function HeroSummary({ estimate, input, onResubmit, isResubmitting = fals
|
||||||
{estimate.median_price_per_m2.toLocaleString("ru-RU")} ₽/м²
|
{estimate.median_price_per_m2.toLocaleString("ru-RU")} ₽/м²
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ export function OfferCard({ estimate, brandSlug }: Props) {
|
||||||
</div>
|
</div>
|
||||||
<div className="card-meta">
|
<div className="card-meta">
|
||||||
<div>
|
<div>
|
||||||
Оценка по рынку:{" "}
|
Цена в объявлении:{" "}
|
||||||
<b className="mono">
|
<b className="mono">
|
||||||
{estimate.insufficient_data ? "—" : `${formatMln(estimate.median_price_rub)} ₽`}
|
{estimate.insufficient_data ? "—" : `${formatMln(estimate.median_price_rub)} ₽`}
|
||||||
</b>
|
</b>
|
||||||
|
|
|
||||||
|
|
@ -756,6 +756,35 @@
|
||||||
font-style: italic;
|
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-duo ── */
|
||||||
.hero-benchmarks {
|
.hero-benchmarks {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -1405,6 +1434,7 @@
|
||||||
.form-card { position: static; max-height: none; }
|
.form-card { position: static; max-height: none; }
|
||||||
.hero-top { grid-template-columns: 1fr; }
|
.hero-top { grid-template-columns: 1fr; }
|
||||||
.hero-duo { 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-prices { grid-template-columns: 1fr; }
|
||||||
.hero-photo { aspect-ratio: 16/9; }
|
.hero-photo { aspect-ratio: 16/9; }
|
||||||
.progress-list { grid-template-columns: 1fr; }
|
.progress-list { grid-template-columns: 1fr; }
|
||||||
|
|
|
||||||
|
|
@ -432,6 +432,20 @@ export default function ResultPanel({
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* §1.2 честный разброс: диапазоны цен = рыночный спред, НЕ погрешность
|
||||||
|
оценки. Ставим сразу под ценовыми карточками, чтобы широкий диапазон
|
||||||
|
не читался как «неуверенность расчёта». */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: 10,
|
||||||
|
lineHeight: 1.5,
|
||||||
|
color: hint,
|
||||||
|
marginTop: -4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Диапазоны показывают разброс цен на рынке, а не погрешность оценки.
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* ranges + radar */}
|
{/* ranges + radar */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ export const resultCards: ResultCard[] = [
|
||||||
range: "6,83 — 12,81 млн ₽",
|
range: "6,83 — 12,81 млн ₽",
|
||||||
ppm: "199 973 ₽/м²",
|
ppm: "199 973 ₽/м²",
|
||||||
delta: "−11%",
|
delta: "−11%",
|
||||||
deltaLabel: "К РЫНКУ",
|
deltaLabel: "К ОБЪЯВЛ.",
|
||||||
nav: 2,
|
nav: 2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -855,7 +855,9 @@ export function mapResultPanel(
|
||||||
e.asking_to_sold_ratio != null
|
e.asking_to_sold_ratio != null
|
||||||
? fmtPct((e.asking_to_sold_ratio - 1) * 100)
|
? fmtPct((e.asking_to_sold_ratio - 1) * 100)
|
||||||
: "—",
|
: "—",
|
||||||
deltaLabel: "К РЫНКУ",
|
// §1.3: разрыв «цена в объявлении → ожидаемая сделка», НЕ скидка сервиса.
|
||||||
|
// «К ОБЪЯВЛ.» однозначнее прежнего «К РЫНКУ» (не читается как «скидка от нас»).
|
||||||
|
deltaLabel: "К ОБЪЯВЛ.",
|
||||||
note:
|
note:
|
||||||
dealTier != null &&
|
dealTier != null &&
|
||||||
e.expected_sold_price_rub != null &&
|
e.expected_sold_price_rub != null &&
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue