diff --git a/frontend/src/components/site-finder/MarketTab.tsx b/frontend/src/components/site-finder/MarketTab.tsx index 85f92eae..52901e58 100644 --- a/frontend/src/components/site-finder/MarketTab.tsx +++ b/frontend/src/components/site-finder/MarketTab.tsx @@ -14,6 +14,35 @@ 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; @@ -26,8 +55,186 @@ export function MarketTab({ data }: Props) { hasVelocity || data.competitors.length > 0; + // ── Above-the-fold values ──────────────────────────────────────────────── + // Note: site_status field pending fix #2/#3 (backend PR). Using total + // competitors count as proxy for active (строящихся) count for now. + const activeCount = data.competitors.length; + const radiusKm = getRadiusKm(data); + const velocityPerMonth = data.velocity?.monthly_velocity_sqm ?? 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 */} {data.competitors.length > 0 && (