fix(sf-13): MarketTab above-the-fold headline + 3 KPI cards #294
1 changed files with 207 additions and 0 deletions
|
|
@ -14,6 +14,35 @@ interface Props {
|
||||||
data: ParcelAnalysis;
|
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) {
|
export function MarketTab({ data }: Props) {
|
||||||
const hasTrend = "market_trend" in data;
|
const hasTrend = "market_trend" in data;
|
||||||
const hasRecommendation = "success_recommendation" in data;
|
const hasRecommendation = "success_recommendation" in data;
|
||||||
|
|
@ -26,8 +55,186 @@ export function MarketTab({ data }: Props) {
|
||||||
hasVelocity ||
|
hasVelocity ||
|
||||||
data.competitors.length > 0;
|
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 (
|
return (
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: 20 }}>
|
<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 */}
|
{/* Competitors */}
|
||||||
{data.competitors.length > 0 && (
|
{data.competitors.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue