feat(site-finder): per-room bucket breakdown в VelocityBlock (#166)
VelocityBucketStat + optional by_room_bucket fields. 'По комнатности' aggregate table + per-competitor inline mini-row. Backward compat via optional field presence guards. Closes #161 (frontend part) Co-authored-by: lekss361 <claudestars@proton.me>
This commit is contained in:
parent
d7fbaa0528
commit
faa99abb94
2 changed files with 128 additions and 0 deletions
|
|
@ -3,6 +3,17 @@ import type { Velocity } from "@/types/site-finder";
|
|||
import { SectionLabel } from "@/components/ui/SectionLabel";
|
||||
import { EmptyState } from "@/components/ui/EmptyState";
|
||||
|
||||
const BUCKET_ORDER = ["студия", "1", "2", "3", "4+"] as const;
|
||||
type KnownBucket = (typeof BUCKET_ORDER)[number];
|
||||
|
||||
const BUCKET_LABEL: Record<KnownBucket, string> = {
|
||||
студия: "Студия",
|
||||
"1": "1-к",
|
||||
"2": "2-к",
|
||||
"3": "3-к",
|
||||
"4+": "4+",
|
||||
};
|
||||
|
||||
interface VelocityBlockProps {
|
||||
velocity: Velocity | null | undefined;
|
||||
}
|
||||
|
|
@ -121,6 +132,102 @@ export function VelocityBlock({ velocity }: VelocityBlockProps) {
|
|||
({velocity.months_observed} мес)
|
||||
</div>
|
||||
|
||||
{/* By room bucket aggregate */}
|
||||
{velocity.by_room_bucket && (
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<SectionLabel>По комнатности</SectionLabel>
|
||||
<table
|
||||
style={{
|
||||
width: "100%",
|
||||
fontSize: 12,
|
||||
borderCollapse: "collapse",
|
||||
marginTop: 4,
|
||||
}}
|
||||
>
|
||||
<thead>
|
||||
<tr
|
||||
style={{ color: "#6b7280", borderBottom: "1px solid #e5e7eb" }}
|
||||
>
|
||||
<th
|
||||
style={{
|
||||
textAlign: "left",
|
||||
padding: "4px 8px",
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
Комната
|
||||
</th>
|
||||
<th
|
||||
style={{
|
||||
textAlign: "right",
|
||||
padding: "4px 8px",
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
Сделок
|
||||
</th>
|
||||
<th
|
||||
style={{
|
||||
textAlign: "right",
|
||||
padding: "4px 8px",
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
м²
|
||||
</th>
|
||||
<th
|
||||
style={{
|
||||
textAlign: "right",
|
||||
padding: "4px 8px",
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
ЖК
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{BUCKET_ORDER.map((b) => {
|
||||
const stat = velocity.by_room_bucket?.[b];
|
||||
if (!stat) return null;
|
||||
return (
|
||||
<tr key={b} style={{ borderBottom: "1px solid #f3f4f6" }}>
|
||||
<td style={{ padding: "4px 8px" }}>{BUCKET_LABEL[b]}</td>
|
||||
<td
|
||||
style={{
|
||||
padding: "4px 8px",
|
||||
textAlign: "right",
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{stat.units.toLocaleString("ru")}
|
||||
</td>
|
||||
<td
|
||||
style={{
|
||||
padding: "4px 8px",
|
||||
textAlign: "right",
|
||||
color: "#6b7280",
|
||||
}}
|
||||
>
|
||||
{Math.round(stat.sqm).toLocaleString("ru")}
|
||||
</td>
|
||||
<td
|
||||
style={{
|
||||
padding: "4px 8px",
|
||||
textAlign: "right",
|
||||
color: "#6b7280",
|
||||
}}
|
||||
>
|
||||
{stat.complexes_count}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Top competitors */}
|
||||
{velocity.sample_competitors.length > 0 && (
|
||||
<div>
|
||||
|
|
@ -188,6 +295,19 @@ export function VelocityBlock({ velocity }: VelocityBlockProps) {
|
|||
{c.obj_class}
|
||||
</span>
|
||||
)}
|
||||
{c.by_room_bucket && (
|
||||
<div
|
||||
style={{ fontSize: 10, color: "#9ca3af", marginTop: 2 }}
|
||||
>
|
||||
{BUCKET_ORDER.filter(
|
||||
(b) => (c.by_room_bucket?.[b] ?? 0) > 0,
|
||||
)
|
||||
.map(
|
||||
(b) => `${BUCKET_LABEL[b]} ${c.by_room_bucket![b]}`,
|
||||
)
|
||||
.join(" · ")}
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td
|
||||
style={{
|
||||
|
|
|
|||
|
|
@ -205,6 +205,12 @@ export interface Pipeline24mo {
|
|||
}
|
||||
|
||||
// D2 (#34) — velocity-score: темп продаж конкурентов
|
||||
export interface VelocityBucketStat {
|
||||
units: number;
|
||||
sqm: number;
|
||||
complexes_count: number;
|
||||
}
|
||||
|
||||
export interface VelocityCompetitor {
|
||||
obj_id: number;
|
||||
name: string | null;
|
||||
|
|
@ -213,6 +219,7 @@ export interface VelocityCompetitor {
|
|||
total_sqm_period: number;
|
||||
dev_name?: string | null;
|
||||
district_name?: string | null;
|
||||
by_room_bucket?: Record<string, number>;
|
||||
}
|
||||
|
||||
export interface VelocityPeriod {
|
||||
|
|
@ -229,6 +236,7 @@ export interface Velocity {
|
|||
months_observed: number;
|
||||
period: VelocityPeriod;
|
||||
sample_competitors: VelocityCompetitor[];
|
||||
by_room_bucket?: Record<string, VelocityBucketStat>;
|
||||
}
|
||||
|
||||
// G5 (#32) — Gate verdict: can_build_mkd
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue