"use client"; /** * SourcesProgress — карточка «Шаг B · Агрегация» под mockup tradein.html. * * Показывает статус 7 строк-источников. Реально работают 4: Cian + Avito + * Yandex + N1. ДомКлик и Restate пока не реализованы — показываем idle для * соответствия макету. */ import type { AggregatedEstimate } from "@/types/trade-in"; interface Props { estimate: AggregatedEstimate | null; isPending: boolean; } interface SourceRow { key: string; label: string; dotClass: string; status: "done" | "loading" | "error" | "idle"; count?: number; median?: number; } function formatMln(rub: number): string { return `${(rub / 1_000_000).toFixed(2).replace(".", ",")} млн`; } export function SourcesProgress({ estimate, isPending }: Props) { // Маппинг реальных данных из estimate на 7 строк-источников const used = new Set(estimate?.sources_used ?? []); const isDone = estimate !== null && !isPending; // Считаем сколько лотов из какого источника const countBySource: Record = {}; if (estimate) { for (const a of estimate.analogs) { if (a.source) { countBySource[a.source] = (countBySource[a.source] ?? 0) + 1; } } } const rows: SourceRow[] = [ { key: "cian", label: "Циан", dotClass: "cian", status: used.has("cian") ? "done" : (isPending ? "loading" : "idle"), count: countBySource.cian, median: used.has("cian") ? estimate?.median_price_rub : undefined, }, { key: "avito", label: "Авито Оценка", dotClass: "avito", status: used.has("avito") ? "done" : (isPending ? "loading" : "idle"), count: countBySource.avito, median: used.has("avito") ? estimate?.median_price_rub : undefined, }, { key: "domklik", label: "ДомКлик Прайс", dotClass: "dom", status: isPending ? "loading" : "idle", }, { key: "yandex", label: "Я.Недвижимость", dotClass: "yandex", status: used.has("yandex") ? "done" : (isPending ? "loading" : "idle"), count: countBySource.yandex, median: used.has("yandex") ? estimate?.median_price_rub : undefined, }, { key: "restate", label: "Restate", dotClass: "etagi", status: isPending ? "loading" : "idle", }, { key: "n1", label: "N1.ru", dotClass: "n1", status: used.has("n1") ? "done" : (isPending ? "loading" : "idle"), count: countBySource.n1, median: used.has("n1") ? estimate?.median_price_rub : undefined, }, { key: "rosreestr", label: "Росреестр (внутр.)", dotClass: "ros", status: isDone && (estimate?.actual_deals.length ?? 0) > 0 ? "done" : (isPending ? "loading" : "idle"), count: estimate?.actual_deals.length, }, ]; const doneCount = rows.filter((r) => r.status === "done").length; const totalCount = rows.length; const overallPct = isDone ? 100 : Math.min(95, (doneCount / totalCount) * 100 + (isPending ? 10 : 0)); return (
Шаг B · Агрегация

Сбор данных по квартире

{doneCount} / {totalCount} источников
{estimate && (
медиана:{" "} {estimate.insufficient_data ? "—" : `${formatMln(estimate.median_price_rub)} ₽`}
)}
{isPending ? "Идёт параллельный запрос — Celery group, timeout 30 сек." : isDone ? "Готово. Частичные результаты доступны при недоступных источниках." : "Введите параметры квартиры и нажмите «Оценить»."}
{rows.map((r) => (
{r.status === "done" && ( )} {r.status === "loading" && ( )} {r.status === "error" && ( )} {r.status === "idle" && ( )} {r.label} {r.status === "done" && r.count !== undefined && ( <> {r.count} лотов )} {r.status === "done" && r.count === undefined && готово} {r.status === "loading" && "сбор..."} {r.status === "error" && "timeout — нет ответа"} {r.status === "idle" && ( {isDone ? "нет данных" : "ожидает запрос"} )}
))}
CACHE-KEY ·{" "} {estimate?.estimate_id?.slice(0, 18) ?? "—"} {" "} ·{" "} при недоступности источника частичный результат не блокируется
); }