feat(tradein): HeroSummary empty-state при insufficient_data (#740) (#743)
All checks were successful
Deploy Trade-In / changes (push) Successful in 4s
Deploy Trade-In / build-frontend (push) Successful in 1m34s
Deploy Trade-In / deploy (push) Successful in 34s
Deploy Trade-In / test (push) Has been skipped
Deploy Trade-In / build-backend (push) Has been skipped
All checks were successful
Deploy Trade-In / changes (push) Successful in 4s
Deploy Trade-In / build-frontend (push) Successful in 1m34s
Deploy Trade-In / deploy (push) Successful in 34s
Deploy Trade-In / test (push) Has been skipped
Deploy Trade-In / build-backend (push) Has been skipped
Co-authored-by: bot-frontend <bot-frontend@gendsgn.local> Co-committed-by: bot-frontend <bot-frontend@gendsgn.local>
This commit is contained in:
parent
34a13d71c3
commit
798fb12b4c
3 changed files with 51 additions and 27 deletions
|
|
@ -95,6 +95,9 @@ export function HeroSummary({ estimate, input, onResubmit, isResubmitting = fals
|
||||||
const m = estimate.median_price_rub;
|
const m = estimate.median_price_rub;
|
||||||
const lo = estimate.range_low_rub;
|
const lo = estimate.range_low_rub;
|
||||||
const hi = estimate.range_high_rub;
|
const hi = estimate.range_high_rub;
|
||||||
|
// Пустая оценка (backend #697): нет аналогов/сделок → median<=0. Вместо
|
||||||
|
// «0,00 млн ₽» в headline показываем явный empty-state.
|
||||||
|
const insufficientData = estimate.insufficient_data || m <= 0;
|
||||||
|
|
||||||
// ── Ожидаемая цена продажи (S3). Рисуем второй блок только при валидном числе:
|
// ── Ожидаемая цена продажи (S3). Рисуем второй блок только при валидном числе:
|
||||||
// null/undefined/0 (старые оценки или пустая ratio-таблица) → одиночный layout. ──
|
// null/undefined/0 (старые оценки или пустая ratio-таблица) → одиночный layout. ──
|
||||||
|
|
@ -294,33 +297,47 @@ export function HeroSummary({ estimate, input, onResubmit, isResubmitting = fals
|
||||||
запроса, с −X%). Без него (старые оценки / пустая ratio) — headline =
|
запроса, с −X%). Без него (старые оценки / пустая ratio) — headline =
|
||||||
asking-медиана, чтобы блок не оставался без главного числа. */}
|
asking-медиана, чтобы блок не оставался без главного числа. */}
|
||||||
<div className="hero-headline">
|
<div className="hero-headline">
|
||||||
<div className="headline-label">
|
{insufficientData ? (
|
||||||
{hasSold ? "Ожидаемая цена сделки" : "Рекомендованная цена"}
|
<>
|
||||||
{showDiscount && (
|
<div className="headline-label">Оценка недоступна</div>
|
||||||
<span
|
<div className="headline-value-empty">Недостаточно данных для оценки</div>
|
||||||
className="headline-delta"
|
<p className="headline-note">
|
||||||
aria-label={`на ${discountPct}% ниже рекомендованной цены в объявлении`}
|
Рядом не нашлось достаточно сопоставимых объявлений и сделок, чтобы
|
||||||
>
|
рассчитать цену. Уточните адрес или параметры объекта и попробуйте
|
||||||
−{discountPct}% к объявлению
|
снова.
|
||||||
</span>
|
</p>
|
||||||
)}
|
</>
|
||||||
</div>
|
) : (
|
||||||
<div className="headline-value mono">
|
<>
|
||||||
{formatMln(hasSold ? (sold as number) : m)} ₽
|
<div className="headline-label">
|
||||||
</div>
|
{hasSold ? "Ожидаемая цена сделки" : "Рекомендованная цена"}
|
||||||
{hasSold && typeof soldLo === "number" && typeof soldHi === "number" && (
|
{showDiscount && (
|
||||||
<div className="headline-range mono">
|
<span
|
||||||
ожидаемый диапазон {formatMln(soldLo)} – {formatMln(soldHi)} ₽
|
className="headline-delta"
|
||||||
{typeof soldPerM2 === "number"
|
aria-label={`на ${discountPct}% ниже рекомендованной цены в объявлении`}
|
||||||
? ` · ${soldPerM2.toLocaleString("ru-RU")} ₽/м²`
|
>
|
||||||
: ""}
|
−{discountPct}% к объявлению
|
||||||
</div>
|
</span>
|
||||||
)}
|
)}
|
||||||
{hasSold && (
|
</div>
|
||||||
<p className="headline-note">
|
<div className="headline-value mono">
|
||||||
Реальные сделки проходят на <b>5–12% ниже</b> цен в объявлениях — это
|
{formatMln(hasSold ? (sold as number) : m)} ₽
|
||||||
ожидаемая цена сделки по данным ДКП Росреестра, а не цена запроса.
|
</div>
|
||||||
</p>
|
{hasSold && typeof soldLo === "number" && typeof soldHi === "number" && (
|
||||||
|
<div className="headline-range mono">
|
||||||
|
ожидаемый диапазон {formatMln(soldLo)} – {formatMln(soldHi)} ₽
|
||||||
|
{typeof soldPerM2 === "number"
|
||||||
|
? ` · ${soldPerM2.toLocaleString("ru-RU")} ₽/м²`
|
||||||
|
: ""}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{hasSold && (
|
||||||
|
<p className="headline-note">
|
||||||
|
Реальные сделки проходят на <b>5–12% ниже</b> цен в объявлениях — это
|
||||||
|
ожидаемая цена сделки по данным ДКП Росреестра, а не цена запроса.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -705,6 +705,12 @@
|
||||||
letter-spacing: -0.025em;
|
letter-spacing: -0.025em;
|
||||||
color: var(--fg);
|
color: var(--fg);
|
||||||
}
|
}
|
||||||
|
.headline-value-empty {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.27;
|
||||||
|
color: var(--fg);
|
||||||
|
}
|
||||||
.headline-range {
|
.headline-range {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,7 @@ export interface AggregatedEstimate {
|
||||||
confidence: ConfidenceLevel;
|
confidence: ConfidenceLevel;
|
||||||
confidence_explanation: string | null;
|
confidence_explanation: string | null;
|
||||||
n_analogs: number;
|
n_analogs: number;
|
||||||
|
insufficient_data: boolean; // backend #697: true когда median_price_rub <= 0 (нет данных)
|
||||||
period_months: number; // 24
|
period_months: number; // 24
|
||||||
analogs: AnalogLot[]; // top 5-10
|
analogs: AnalogLot[]; // top 5-10
|
||||||
actual_deals: AnalogLot[]; // last 12 mo
|
actual_deals: AnalogLot[]; // last 12 mo
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue