"use client"; /** * HeroSummary — Секция 1 «Сводка» из mockup tradein.html. * Показывает медиану + достоверность CV + параметры объекта + 2 ценовых бара (объявления / сделки). */ import type { AggregatedEstimate, TradeInEstimateInput } from "@/types/trade-in"; interface Props { estimate: AggregatedEstimate; input: TradeInEstimateInput; } const HOUSE_TYPE_LABELS: Record = { panel: "Панельный", brick: "Кирпичный", monolith: "Монолит", monolith_brick: "Монолит-кирпич", other: "Другое", }; const REPAIR_LABELS: Record = { needs_repair: "Требует ремонта", standard: "Стандартный", good: "Хороший", excellent: "Евроремонт", }; const CONF_LABELS: Record = { high: { txt: "высокая", color: "var(--success)" }, medium: { txt: "средняя", color: "var(--success)" }, low: { txt: "низкая", color: "var(--danger)" }, }; function formatMln(rub: number): string { return `${(rub / 1_000_000).toFixed(2).replace(".", ",")} млн`; } function calcCv(estimate: AggregatedEstimate): number { // CV = (P75 - P25) / median * 100 (наш range_high - range_low = P25-P75 пара) const m = estimate.median_price_rub; if (m === 0) return 0; return ((estimate.range_high_rub - estimate.range_low_rub) / m) * 100; } export function HeroSummary({ estimate, input }: Props) { const cv = calcCv(estimate); const conf = CONF_LABELS[estimate.confidence] ?? CONF_LABELS.low; const m = estimate.median_price_rub; const lo = estimate.range_low_rub; const hi = estimate.range_high_rub; // Расчёт ширины для price bar (50% = середина): медиана внутри min/max const span = hi - lo; const medianPctRaw = span > 0 ? ((m - lo) / span) * 100 : 50; const medianPct = Math.max(5, Math.min(95, medianPctRaw)); return (
Секция 1 · Сводка

Анализ рынка и расчёт стоимости

Источник:{" "} агрегация {estimate.sources_used.length}/7
Достоверность · {conf.txt} · CV {cv.toFixed(1)}%
{estimate.sources_used.length > 0 ? `${estimate.sources_used[0]} · ${estimate.n_analogs} аналогов` : "Нет фото"}
{estimate.target_address ?? input.address}
ЭТАЖ {input.floor}/{input.total_floors} {input.year_built ? ` · ${input.year_built}` : ""}
{input.year_built && (
Год постройки {input.year_built}
)} {input.house_type && (
Тип дома {HOUSE_TYPE_LABELS[input.house_type] ?? input.house_type}
)}
Этаж {input.floor} / {input.total_floors}
Площадь {input.area_m2} м²
Планировка {input.rooms === 0 ? "Студия" : `${input.rooms}-к`}, классическая
{input.repair_state && (
Состояние {REPAIR_LABELS[input.repair_state] ?? input.repair_state}
)}
Балкон {input.has_balcony ? "есть" : "нет"}
Аналогов {estimate.n_analogs}
Диапазон цен в объявлениях{" "} (без учёта ремонта) медиана · {formatMln(m)} ₽
{formatMln(lo)}
{formatMln(hi)}
{formatMln(m)} ₽
{estimate.median_price_per_m2.toLocaleString("ru-RU")} ₽/м²
{estimate.n_analogs} аналог.
{estimate.actual_deals.length > 0 && (() => { const dealsPrices = estimate.actual_deals.map((d) => d.price_rub); const dLo = Math.min(...dealsPrices); const dHi = Math.max(...dealsPrices); const dM = dealsPrices.sort((a, b) => a - b)[Math.floor(dealsPrices.length / 2)]; const dSpan = dHi - dLo; const dMedPct = dSpan > 0 ? Math.max(5, Math.min(95, ((dM - dLo) / dSpan) * 100)) : 50; return (
Диапазон цен по фактическим сделкам медиана · {formatMln(dM)} ₽
{formatMln(dLo)}
{formatMln(dHi)}
{formatMln(dM)} ₽
{estimate.actual_deals.length} сделок
Росреестр · ДомКлик
); })()}
{estimate.confidence_explanation && (
{estimate.confidence_explanation}
Цены в объявлениях ≠ реальная сделка — разница 5–12% по данным Росреестра.
Самостоятельная продажа = до 15% потерь на торге, риелторе, нотариусе и аренде.
)}
); }