gendesign/tradein-mvp/frontend/src/app/history/page.tsx
bot-frontend 5af7542522 fix(tradein): resilience-хвост аудита (#770 findings #26-33 + global-error)
#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
2026-05-30 20:09:16 +03:00

87 lines
3.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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>
);
}