From 34b4a3ba4e6c54a8e021386327304797de54f69c Mon Sep 17 00:00:00 2001 From: lekss361 Date: Sun, 17 May 2026 17:02:21 +0300 Subject: [PATCH] fix(sf-13): MarketTab above-the-fold headline-bar + 3 KPI cards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add headline sentence (N строящихся · велосити · mix%) and a 3-card KPI row (Активные / Велосити / Срок продаж) before any other content in MarketTab, so the financial director sees the verdict above the fold without scrolling. - Headline uses var(--bg-headline) dark bar per UI_Brief_GenDesign_May17 - KPI cards: grid-cols-3, bg-white border border-slate-200 rounded-lg p-3 - Derives radius_km from market_trend.radius_km or pipeline_24mo.radius_km - Derives velocity from data.velocity.monthly_velocity_sqm (existing field) - Derives maxMixPct from success_recommendation.ranking max success_score - Derives salesPeriodMonths from pipeline_24mo.flats_total / velocity - site_status filter deferred to fix #2/#3 (field not yet in type) --- .../src/components/site-finder/MarketTab.tsx | 207 ++++++++++++++++++ 1 file changed, 207 insertions(+) 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 && (