Backend (parcels.py): - Запрос к domrf_kn_objects в радиусе 5км с ready_dt BETWEEN NOW() AND NOW()+24mo. - _aggregate_pipeline() — сводка: objects_count, flats_total, by_class (эконом/ комфорт/бизнес/...), by_quarter (хронологически, для UI bar), severity (low <500 / medium <3000 / high) per spec, top_objects (десятка по flat_count desc). - Поле analyze.pipeline_24mo. Backward-compat — optional. Frontend: - Pipeline24moBlock.tsx — severity badge + 3 summary numbers (объектов, квартир, горизонт/радиус), by-class chips, гистограмма bar по кварталам сдачи (нормирована на max), разворачиваемый top-N список с классом + датой сдачи. - Добавлен в MarketTab выше "Market trend". - TS типы: Pipeline24mo, PipelineObject, PipelineQuarterSlot. Closes #36. Relates to #19 (Конкурентный 360 — закрыт ранее в #36 scope).
96 lines
2.5 KiB
TypeScript
96 lines
2.5 KiB
TypeScript
"use client";
|
||
|
||
import type { ParcelAnalysis } from "@/types/site-finder";
|
||
import { MarketTrendBlock } from "./MarketTrendBlock";
|
||
import { CompetitorTable } from "./CompetitorTable";
|
||
import { Pipeline24moBlock } from "./Pipeline24moBlock";
|
||
import { SuccessRecommendationBlock } from "./SuccessRecommendationBlock";
|
||
|
||
interface Props {
|
||
data: ParcelAnalysis;
|
||
}
|
||
|
||
export function MarketTab({ data }: Props) {
|
||
const hasTrend = "market_trend" in data;
|
||
const hasRecommendation = "success_recommendation" in data;
|
||
const hasPipeline = data.pipeline_24mo !== undefined;
|
||
const hasAny =
|
||
hasTrend || hasRecommendation || hasPipeline || data.competitors.length > 0;
|
||
|
||
return (
|
||
<div style={{ display: "flex", flexDirection: "column", gap: 20 }}>
|
||
{/* D4 (#36) — Pipeline 24mo */}
|
||
{data.pipeline_24mo && <Pipeline24moBlock data={data.pipeline_24mo} />}
|
||
|
||
{/* Market trend */}
|
||
{hasTrend && (
|
||
<div
|
||
style={{
|
||
border: "1px solid #e5e7eb",
|
||
borderRadius: 10,
|
||
padding: "14px 18px",
|
||
background: "#fff",
|
||
}}
|
||
>
|
||
<MarketTrendBlock trend={data.market_trend} />
|
||
</div>
|
||
)}
|
||
|
||
{/* Competitors */}
|
||
{data.competitors.length > 0 && (
|
||
<div>
|
||
<div
|
||
style={{
|
||
fontSize: 12,
|
||
fontWeight: 600,
|
||
color: "#6b7280",
|
||
textTransform: "uppercase",
|
||
letterSpacing: "0.05em",
|
||
marginBottom: 12,
|
||
}}
|
||
>
|
||
Конкуренты
|
||
</div>
|
||
<CompetitorTable
|
||
competitors={data.competitors}
|
||
districtName={data.district?.district_name}
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
{/* Success recommendation */}
|
||
{hasRecommendation && (
|
||
<div>
|
||
<div
|
||
style={{
|
||
fontSize: 12,
|
||
fontWeight: 600,
|
||
color: "#6b7280",
|
||
textTransform: "uppercase",
|
||
letterSpacing: "0.05em",
|
||
marginBottom: 12,
|
||
}}
|
||
>
|
||
Что хорошо продаётся
|
||
</div>
|
||
<SuccessRecommendationBlock
|
||
recommendation={data.success_recommendation}
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
{!hasAny && (
|
||
<div
|
||
style={{
|
||
fontSize: 13,
|
||
color: "#9ca3af",
|
||
padding: "24px 0",
|
||
textAlign: "center",
|
||
}}
|
||
>
|
||
Рыночные данные недоступны
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|