fix(tradein): error/not-found/loading UI при restore по ?id=

page.tsx читал только restoredEstimate.data. Backend отдаёт 404 на
missing/owner-mismatch/expired → resultData=null → молчаливый empty-state
«Введите параметры», маскируя «отчёт не найден». Теперь:
- isError → карточка «Отчёт не найден или недоступен» (404 via HTTPError) или
  generic error + кнопка «Новая оценка» (router.replace('/')).
- isPending (restore, не fresh) → «Загрузка отчёта…».
Fresh-оценка (freshResult) и happy-restore не затронуты.

Refs #822
This commit is contained in:
bot-frontend 2026-05-30 22:45:37 +03:00
parent 1a733302c2
commit b8aa77dbe2

View file

@ -12,6 +12,7 @@ import { useQueryClient } from "@tanstack/react-query";
import "@/components/trade-in/trade-in.css";
import type { AggregatedEstimate, TradeInEstimateInput, HouseType, RepairState } from "@/types/trade-in";
import { useEstimateMutation, useEstimate } from "@/lib/trade-in-api";
import { HTTPError } from "@/lib/api";
import { EstimateForm } from "@/components/trade-in/EstimateForm";
import { Topbar } from "@/components/trade-in/Topbar";
import { SourcesProgress } from "@/components/trade-in/SourcesProgress";
@ -127,6 +128,16 @@ export default function TradeInPage() {
}
: null);
// #822: restore по ?id= — отражаем загрузку/ошибку вместо молчаливого
// empty-state. Backend отдаёт 404 на missing/owner-mismatch/expired.
const restoreActive = urlEstimateId !== null && freshResult === null;
const restoreError = restoreActive && restoredEstimate.isError;
const restoreLoading = restoreActive && restoredEstimate.isPending;
const restoreNotFound =
restoreError &&
restoredEstimate.error instanceof HTTPError &&
restoredEstimate.error.status === 404;
// Надёжный input для «Что-если»: предпочитаем поля самой оценки (есть и при
// restore по ?id=, где resultData.input — stub с нулями), откатываемся на input.
// address/rooms обязательны для payload — берём из estimate, иначе из input.
@ -275,6 +286,36 @@ export default function TradeInPage() {
<ExposureCard estimate={resultData.estimate} />
<OfferCard estimate={resultData.estimate} brandSlug={activeBrandSlug} />
</>
) : restoreError ? (
<article className="card">
<div className="card-body" style={{ padding: "48px 24px", textAlign: "center" }}>
<h3 style={{ fontSize: 16, fontWeight: 600, marginBottom: 6 }}>
{restoreNotFound
? "Отчёт не найден или недоступен"
: "Не удалось загрузить отчёт"}
</h3>
<p style={{ color: "var(--muted)", fontSize: 14, marginBottom: 16 }}>
{restoreNotFound
? "Ссылка устарела, отчёт удалён или у вас нет к нему доступа."
: "Произошла ошибка при загрузке отчёта. Попробуйте ещё раз."}
</p>
<button
type="button"
className="btn btn-primary"
onClick={() => router.replace("/")}
>
Новая оценка
</button>
</div>
</article>
) : restoreLoading ? (
<article className="card">
<div className="card-body" style={{ padding: "48px 24px", textAlign: "center" }}>
<p style={{ color: "var(--muted)", fontSize: 14 }}>
Загрузка отчёта
</p>
</div>
</article>
) : (
<article className="card">
<div className="card-body" style={{ padding: "48px 24px", textAlign: "center" }}>