"use client"; import type { Velocity } from "@/types/site-finder"; import { SectionLabel } from "@/components/ui/SectionLabel"; import { EmptyState } from "@/components/ui/EmptyState"; const BUCKET_ORDER = ["студия", "1", "2", "3", "4+"] as const; type KnownBucket = (typeof BUCKET_ORDER)[number]; const BUCKET_LABEL: Record = { студия: "Студия", "1": "1-к", "2": "2-к", "3": "3-к", "4+": "4+", }; interface VelocityBlockProps { velocity: Velocity | null | undefined; } const CONFIDENCE_COLOR: Record< Velocity["confidence"], { bg: string; fg: string } > = { high: { bg: "#dcfce7", fg: "#166534" }, medium: { bg: "#fef3c7", fg: "#854d0e" }, low: { bg: "#fee2e2", fg: "#991b1b" }, }; const CONFIDENCE_LABEL: Record = { high: "Высокая", medium: "Средняя", low: "Низкая", }; function formatPercent(score: number): string { return `${Math.round(score * 100)}%`; } export function VelocityBlock({ velocity }: VelocityBlockProps) { if (!velocity) { return (
Темп продаж конкурентов
); } const dataAvailable = velocity.velocity_data_available !== false; const isRosreestrFallback = velocity.velocity_source === "rosreestr_fallback"; const confColor = CONFIDENCE_COLOR[velocity.confidence]; const scorePct = formatPercent(velocity.velocity_score); const ratio = velocity.monthly_velocity_sqm / velocity.ekb_median_sqm; return (
Темп продаж конкурентов
{!dataAvailable && ( нет данных velocity )} {isRosreestrFallback && ( Источник: квартальные сделки )} {CONFIDENCE_LABEL[velocity.confidence]}
{/* Score gauge — показываем только если данные есть */} {dataAvailable && (
Velocity-score {scorePct}
= 0.66 ? "#10b981" : velocity.velocity_score >= 0.33 ? "#f59e0b" : "#ef4444", width: `${velocity.velocity_score * 100}%`, height: "100%", }} />
{Math.round(velocity.monthly_velocity_sqm)} м²/мес vs{" "} {Math.round(velocity.ekb_median_sqm)} м²/мес (медиана ЕКБ) ·{" "} {ratio >= 1 ? `x${ratio.toFixed(1)} выше` : `${formatPercent(ratio)} от среднего`}
)} {/* Period + competitors meta */}
В радиусе 3 км: {velocity.competitors_count} ЖК {dataAvailable && ( <> {" "} · период{" "} {velocity.period.start} → {velocity.period.end} {" "} ({velocity.months_observed} мес) )}
{/* By room bucket aggregate */} {velocity.by_room_bucket && (
По комнатности {BUCKET_ORDER.map((b) => { const stat = velocity.by_room_bucket?.[b]; if (!stat) return null; return ( ); })}
Комната Сделок м² ЖК
{BUCKET_LABEL[b]} {stat.units.toLocaleString("ru")} {Math.round(stat.sqm).toLocaleString("ru")} {stat.complexes_count}
)} {/* Top competitors */} {velocity.sample_competitors.length > 0 && (
Топ продавцов {velocity.sample_competitors.map((c) => ( ))}
Название Расст. Продано м²
{c.name ?? `obj#${c.obj_id}`} {c.obj_class && ( {c.obj_class} )} {c.by_room_bucket && (
{BUCKET_ORDER.filter( (b) => (c.by_room_bucket?.[b] ?? 0) > 0, ) .map( (b) => `${BUCKET_LABEL[b]} ${c.by_room_bucket![b]}`, ) .join(" · ")}
)}
{Math.round(c.distance_m)}м {Math.round(c.total_sqm_period).toLocaleString("ru")}
)}
); }