"use client"; import type { ParcelAnalysis } from "@/types/site-finder"; import { SectionLabel } from "@/components/ui/SectionLabel"; import { EmptyState } from "@/components/ui/EmptyState"; import { MarketTrendBlock } from "./MarketTrendBlock"; import { BestLayoutsBlock } from "./BestLayoutsBlock"; import { CompetitorTable } from "./CompetitorTable"; import { Pipeline24moBlock } from "./Pipeline24moBlock"; import { SuccessRecommendationBlock } from "./SuccessRecommendationBlock"; import { VelocityBlock } from "./VelocityBlock"; interface Props { data: ParcelAnalysis; } /** Derive radius_km from available nested fields. */ function getRadiusKm(data: ParcelAnalysis): number { return data.market_trend?.radius_km ?? data.pipeline_24mo?.radius_km ?? 3; } /** * Max success score (%) from success_recommendation ranking. * Returns null when recommendation is absent or ranking is empty. */ function getMaxMixPct(data: ParcelAnalysis): number | null { const ranking = data.success_recommendation?.ranking; if (!ranking || ranking.length === 0) return null; const maxScore = Math.max(...ranking.map((r) => r.success_score)); return Math.round(maxScore * 100); } /** * Estimated sales period in months (supply / flats_per_month). * Uses pipeline_24mo.flats_total as supply proxy, velocity for rate. * Assumes ~50 m² avg flat for unit conversion. */ function getSalesPeriodMonths(data: ParcelAnalysis): number | null { const supply = data.pipeline_24mo?.flats_total; const velocitySqm = data.velocity?.monthly_velocity_sqm; if (!supply || !velocitySqm || velocitySqm <= 0) return null; const flatsPerMonth = velocitySqm / 50; return Math.round(supply / flatsPerMonth); } export function MarketTab({ data }: Props) { const hasTrend = "market_trend" in data; const hasRecommendation = "success_recommendation" in data; const hasPipeline = data.pipeline_24mo !== undefined; const hasVelocity = "velocity" in data; const hasAny = hasTrend || hasRecommendation || hasPipeline || hasVelocity || data.competitors.length > 0; // ── Above-the-fold values ──────────────────────────────────────────────── // Maxim's spec: «ЖК старше года → не конкурент». Filter active competitors: // - site_status === 'Строящиеся' → активен // - либо ready_dt в будущем → активен (готовность ещё не наступила) // - либо ready_dt в пределах последнего года → активен (свежие продажи) // - иначе (Сданные >1y назад) → не конкурент, скрыт const REL_MS = 365 * 24 * 60 * 60 * 1000; const yearAgoTs = Date.now() - REL_MS; const relevantCompetitors = data.competitors.filter((c) => { if (c.site_status === "Строящиеся") return true; if (!c.ready_dt) return false; const ts = new Date(c.ready_dt).getTime(); if (Number.isNaN(ts)) return false; return ts >= yearAgoTs; }); const activeCount = relevantCompetitors.length; const radiusKm = getRadiusKm(data); // Treat zero velocity as "no data" — backend returns 0 when competitors // are unmapped to Objective ground truth (OBJ-3). Showing "0.0 м²/мес" // is misleading; "—" makes it clear data is unavailable. const velocityRaw = data.velocity?.monthly_velocity_sqm; const velocityPerMonth = velocityRaw != null && velocityRaw > 0 ? velocityRaw : null; const maxMixPct = getMaxMixPct(data); const salesPeriodMonths = getSalesPeriodMonths(data); const showHeadlineBar = activeCount > 0 || velocityPerMonth !== null; return (
{/* ── Above-the-fold: Headline-bar + 3 KPI ─────────────────────────── */} {showHeadlineBar && ( <> {/* Headline-bar — one sentence verdict */}
{activeCount} активных в {radiusKm}км {" · велосити "} {velocityPerMonth !== null ? `${velocityPerMonth.toFixed(1)} м²/мес` : "—"} {" · рекомендуемый mix "} {maxMixPct !== null ? `${maxMixPct}%` : "—"}
{/* 3 KPI cards */}
{/* KPI 1: Активные ЖК */}
Активные ЖК
{activeCount}
строящихся и недавно сданных в радиусе {radiusKm}км
{/* KPI 2: Велосити */}
Велосити
{velocityPerMonth !== null ? velocityPerMonth.toFixed(1) : "—"} {velocityPerMonth !== null && ( м²/мес )}
ср. темп продаж конкурентов
{/* KPI 3: Срок продаж */}
Срок продаж
{salesPeriodMonths !== null ? salesPeriodMonths : "—"} {salesPeriodMonths !== null && ( мес )}
supply / велосити (оценка)
)} {/* ── Main content blocks ───────────────────────────────────────────── */} {/* Competitors */} {relevantCompetitors.length > 0 && (
Конкуренты ({relevantCompetitors.length} из{" "} {data.competitors.length} {" — фильтр: строящиеся или сданы ≤1 года)"}
)} {/* Issue #113 — data-driven ТЗ на проектирование */} {/* D2 (#34) — Velocity-score */} {hasVelocity && (
)} {/* Market trend */} {hasTrend && (
)} {/* D4 (#36) — Pipeline 24mo */} {data.pipeline_24mo && } {/* Success recommendation */} {hasRecommendation && (
Что хорошо продаётся
)} {!hasAny && }
); }