#26 QueryClient: не ретраить 4xx/429 (anti-storm), mutations retry:false. #27 WhatIfPanel: seq-guard на out-of-order пересчёты. #28 WhatIfPanel: caveat-title на дельту (включает scrape-разброс). #29 стабильные React keys в ListingsCard/DealsCard (composite вместо index). #30 asHouseType/asRepairState — runtime-валидация перед cast (EstimateForm/HeroSummary). #31 humanizeAgo: мин→ч→дн для data_freshness_minutes (ListingsCard). #32 DealsCard: period_months вместо хардкода «7 месяцев». #33 .gitignore: broader cookies pattern (.avito/.yandex). .bak в git нет — nothing to rm. #25 (частично): global-error.tsx + history error-state. Sub-part ?id-restore error-UI в page.tsx — deferred (нужен UX error-state в restore-флоу). Refs #770
87 lines
3.3 KiB
TypeScript
87 lines
3.3 KiB
TypeScript
"use client";
|
||
|
||
/* История оценок (#399). */
|
||
import { useEffect, useState } from "react";
|
||
|
||
import "@/components/trade-in/trade-in.css";
|
||
import { API_BASE_URL } from "@/lib/api";
|
||
|
||
interface HistoryRow {
|
||
id: string;
|
||
address: string;
|
||
rooms: number;
|
||
area_m2: number;
|
||
median_price: number;
|
||
confidence: string;
|
||
n_analogs: number;
|
||
created_at: string;
|
||
}
|
||
|
||
export default function HistoryPage() {
|
||
const [rows, setRows] = useState<HistoryRow[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
// #770 finding #25: не маскировать ошибку пустым списком — иначе «нет оценок»
|
||
// показывается и при реальном сбое загрузки.
|
||
const [error, setError] = useState(false);
|
||
|
||
useEffect(() => {
|
||
fetch(`${API_BASE_URL}/api/v1/trade-in/history?limit=100`)
|
||
.then((r) => {
|
||
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
||
return r.json();
|
||
})
|
||
.then((d) => setRows(d as HistoryRow[]))
|
||
.catch(() => setError(true))
|
||
.finally(() => setLoading(false));
|
||
}, []);
|
||
|
||
return (
|
||
<main className="page" style={{ padding: 24, maxWidth: 1000, margin: "0 auto" }}>
|
||
<p style={{ marginBottom: 12 }}>
|
||
<a href={`${API_BASE_URL}/`}>← К оценке</a>
|
||
</p>
|
||
<h1 style={{ fontSize: 20, fontWeight: 700, marginBottom: 16 }}>История оценок</h1>
|
||
|
||
{loading ? (
|
||
<p style={{ color: "var(--muted)" }}>Загрузка…</p>
|
||
) : error ? (
|
||
<p style={{ color: "var(--danger, #b3261e)" }}>
|
||
Не удалось загрузить историю оценок. Обновите страницу или попробуйте позже.
|
||
</p>
|
||
) : rows.length === 0 ? (
|
||
<p style={{ color: "var(--muted)" }}>Оценок пока нет.</p>
|
||
) : (
|
||
<table style={{ width: "100%", borderCollapse: "collapse", fontSize: 14 }}>
|
||
<thead>
|
||
<tr style={{ textAlign: "left", borderBottom: "2px solid var(--border, #ddd)" }}>
|
||
<th style={{ padding: 8 }}>Дата</th>
|
||
<th style={{ padding: 8 }}>Адрес</th>
|
||
<th style={{ padding: 8 }}>Комн.</th>
|
||
<th style={{ padding: 8 }}>Площадь</th>
|
||
<th style={{ padding: 8, textAlign: "right" }}>Медиана, ₽</th>
|
||
<th style={{ padding: 8 }}>Точность</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{rows.map((r) => (
|
||
<tr key={r.id} style={{ borderBottom: "1px solid var(--border, #eee)" }}>
|
||
<td style={{ padding: 8, whiteSpace: "nowrap" }}>
|
||
{new Date(r.created_at).toLocaleDateString("ru-RU")}
|
||
</td>
|
||
<td style={{ padding: 8 }}>
|
||
<a href={`${API_BASE_URL}/?id=${r.id}`}>{r.address}</a>
|
||
</td>
|
||
<td style={{ padding: 8 }}>{r.rooms}</td>
|
||
<td style={{ padding: 8 }}>{r.area_m2} м²</td>
|
||
<td style={{ padding: 8, textAlign: "right" }}>
|
||
{Number(r.median_price).toLocaleString("ru-RU")}
|
||
</td>
|
||
<td style={{ padding: 8 }}>{r.confidence}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
)}
|
||
</main>
|
||
);
|
||
}
|