fix(sf-13): MarketTab above-the-fold headline + 3 KPI cards (#294)
This commit is contained in:
parent
33f44b59cb
commit
4b967343f8
1 changed files with 207 additions and 0 deletions
|
|
@ -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 (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 20 }}>
|
||||
{/* ── Above-the-fold: Headline-bar + 3 KPI ─────────────────────────── */}
|
||||
{showHeadlineBar && (
|
||||
<>
|
||||
{/* Headline-bar — one sentence verdict */}
|
||||
<div
|
||||
style={{
|
||||
background: "var(--bg-headline, #0F172A)",
|
||||
color: "var(--fg-on-dark, #E2E8F0)",
|
||||
borderRadius: 12,
|
||||
padding: "14px 18px",
|
||||
fontSize: 15,
|
||||
fontWeight: 500,
|
||||
lineHeight: 1.4,
|
||||
}}
|
||||
>
|
||||
<strong>
|
||||
{activeCount} строящихся в {radiusKm}км
|
||||
</strong>
|
||||
{" · велосити "}
|
||||
<strong>
|
||||
{velocityPerMonth !== null
|
||||
? `${velocityPerMonth.toFixed(1)} м²/мес`
|
||||
: "—"}
|
||||
</strong>
|
||||
{" · рекомендуемый mix "}
|
||||
<strong>{maxMixPct !== null ? `${maxMixPct}%` : "—"}</strong>
|
||||
</div>
|
||||
|
||||
{/* 3 KPI cards */}
|
||||
<div className="grid grid-cols-3 gap-3 mb-4">
|
||||
{/* KPI 1: Активные ЖК */}
|
||||
<div
|
||||
style={{
|
||||
background: "#fff",
|
||||
border: "1px solid #e6e8ec",
|
||||
borderRadius: 12,
|
||||
padding: "12px 16px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 11,
|
||||
textTransform: "uppercase" as const,
|
||||
letterSpacing: "0.04em",
|
||||
color: "#5b6066",
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
Активные ЖК
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
marginTop: 6,
|
||||
fontSize: 28,
|
||||
fontWeight: 600,
|
||||
color: "#111",
|
||||
fontVariantNumeric: "tabular-nums",
|
||||
}}
|
||||
>
|
||||
{activeCount}
|
||||
</div>
|
||||
<div style={{ marginTop: 4, fontSize: 12, color: "#73767e" }}>
|
||||
строящихся в радиусе {radiusKm}км
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* KPI 2: Велосити */}
|
||||
<div
|
||||
style={{
|
||||
background: "#fff",
|
||||
border: "1px solid #e6e8ec",
|
||||
borderRadius: 12,
|
||||
padding: "12px 16px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 11,
|
||||
textTransform: "uppercase" as const,
|
||||
letterSpacing: "0.04em",
|
||||
color: "#5b6066",
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
Велосити
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
marginTop: 6,
|
||||
display: "flex",
|
||||
alignItems: "baseline",
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 28,
|
||||
fontWeight: 600,
|
||||
color: "#111",
|
||||
fontVariantNumeric: "tabular-nums",
|
||||
}}
|
||||
>
|
||||
{velocityPerMonth !== null
|
||||
? velocityPerMonth.toFixed(1)
|
||||
: "—"}
|
||||
</span>
|
||||
{velocityPerMonth !== null && (
|
||||
<span style={{ fontSize: 13, color: "#5b6066" }}>м²/мес</span>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ marginTop: 4, fontSize: 12, color: "#73767e" }}>
|
||||
ср. темп продаж конкурентов
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* KPI 3: Срок продаж */}
|
||||
<div
|
||||
style={{
|
||||
background: "#fff",
|
||||
border: "1px solid #e6e8ec",
|
||||
borderRadius: 12,
|
||||
padding: "12px 16px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 11,
|
||||
textTransform: "uppercase" as const,
|
||||
letterSpacing: "0.04em",
|
||||
color: "#5b6066",
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
Срок продаж
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
marginTop: 6,
|
||||
display: "flex",
|
||||
alignItems: "baseline",
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 28,
|
||||
fontWeight: 600,
|
||||
color: "#111",
|
||||
fontVariantNumeric: "tabular-nums",
|
||||
}}
|
||||
>
|
||||
{salesPeriodMonths !== null ? salesPeriodMonths : "—"}
|
||||
</span>
|
||||
{salesPeriodMonths !== null && (
|
||||
<span style={{ fontSize: 13, color: "#5b6066" }}>мес</span>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ marginTop: 4, fontSize: 12, color: "#73767e" }}>
|
||||
supply / велосити (оценка)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* ── Main content blocks ───────────────────────────────────────────── */}
|
||||
|
||||
{/* Competitors */}
|
||||
{data.competitors.length > 0 && (
|
||||
<div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue