fix(frontend): карточка «не хватает данных» вместо статичной заглушки
All checks were successful
CI Trade-In / frontend-checks (pull_request) Successful in 1m23s
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 13s
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

Состояние уже существовало и подключалось по insufficient_data, но панель
была без пропсов: один и тот же текст на любой случай, без адреса и без
следующего шага. После бэкендной правки это состояние станет заметно
чаще (общегородской ДКП-коридор перестаёт становиться ценой), и
тестировщик упрётся именно в него.

Теперь карточка показывает объяснение, которое прислал сам бэкенд
(confidence_explanation), найденный геокодером адрес — он заполнен даже
когда цены нет — и кнопку уточнить адрес.

Предикат вынесен в именованную функцию isInsufficientEstimate и опирается
на серверный флаг insufficient_data, а не на price === 0: флаг определён
как «ответ успешный, но median_price_rub <= 0», то есть ровно нужное
состояние, и он не путается с ещё не загруженной оценкой.
This commit is contained in:
bot-backend 2026-09-16 19:53:52 +03:00
parent 0c6b5a4548
commit 55e0d9e174

View file

@ -289,12 +289,63 @@ function EmptyResultPanel() {
); );
} }
function InsufficientPanel() { // Named predicate for the "no price at all" state — deliberately NOT
// `!estimate.median_price_rub` (a bare zero/price check): that would also
// misfire on a still-loading `estimate` or on legitimate falsy-but-present
// values. The one reliable signal is the backend's own `insufficient_data`
// flag, which is defined (see AggregatedEstimate in types/trade-in.ts /
// backend @computed_field) to mean "this IS a successful response, but
// median_price_rub <= 0 — there was nothing to price with". Only fires on a
// present, successfully-returned estimate.
function isInsufficientEstimate(estimate: AggregatedEstimate | null): boolean {
return estimate != null && estimate.insufficient_data === true;
}
// Карточка состояния «цены нет». Показывает объяснение, которое прислал сам
// бэкенд (confidence_explanation — текст написан специально под это состояние),
// и адрес, который геокодер всё-таки нашёл: target_address и координаты
// заполнены даже когда цены нет. Плюс понятное следующее действие.
// Цена, диапазон, «N% к рынку» и аналоги здесь не рендерятся вовсе — на
// проводе они 0/null, и показать их значило бы выдать сломанный экран «0 ₽»
// вместо честного «дом нашли, цену — нет».
function InsufficientPanel({
estimate,
onNew,
}: {
estimate: AggregatedEstimate;
onNew: () => void;
}) {
return ( return (
<PlaceholderPanel <PlaceholderPanel
title="НЕДОСТАТОЧНО ДАННЫХ" title="НЕ ХВАТАЕТ ДАННЫХ ДЛЯ ОЦЕНКИ"
body="По этому адресу не нашлось достаточного числа аналогов и сделок для надёжной оценки. Уточните адрес или параметры квартиры." body={
/> estimate.confidence_explanation ??
"По этому адресу не нашлось достаточного числа аналогов и сделок для надёжной оценки. Уточните адрес или параметры квартиры."
}
>
{estimate.target_address && (
<div
style={{
fontSize: 12,
color: tokens.ink,
maxWidth: 360,
lineHeight: 1.5,
}}
>
Найденный адрес: {estimate.target_address}
</div>
)}
<div style={{ display: "flex", gap: 10, marginTop: 4 }}>
<button
type="button"
className="v2-retry-btn"
onClick={onNew}
style={retryBtnStyle}
>
УТОЧНИТЬ АДРЕС
</button>
</div>
</PlaceholderPanel>
); );
} }
@ -588,7 +639,7 @@ export default function TradeInV2Page() {
// existing Серов estimates were already stuck in this state). See // existing Серов estimates were already stuck in this state). See
// mapResultPanel's `dealsOnlyPrice` for how the result panel discloses the // mapResultPanel's `dealsOnlyPrice` for how the result panel discloses the
// deals-only source instead of mislabelling it as "по объявлениям". // deals-only source instead of mislabelling it as "по объявлениям".
const insufficient = estimate != null && estimate.insufficient_data; const insufficient = isInsufficientEstimate(estimate);
const apiError = mutation.error?.message ?? null; const apiError = mutation.error?.message ?? null;
// M4: estimate-dependent meta/controls (the «ДЕЙСТВИТЕЛЕН ДО» validity line, // M4: estimate-dependent meta/controls (the «ДЕЙСТВИТЕЛЕН ДО» validity line,
// «КАК РАССЧИТАНО», PDF) only render once there is a real, sufficient estimate. // «КАК РАССЧИТАНО», PDF) only render once there is a real, sufficient estimate.
@ -830,7 +881,7 @@ export default function TradeInV2Page() {
</div> </div>
); );
} else if (estimate && insufficient) { } else if (estimate && insufficient) {
middleContent = <InsufficientPanel />; middleContent = <InsufficientPanel estimate={estimate} onNew={handleNew} />;
} else { } else {
middleContent = <EmptyResultPanel />; middleContent = <EmptyResultPanel />;
} }