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 { SectionLabel } from "@/components/ui/SectionLabel";
|
||||||
import { EmptyState } from "@/components/ui/EmptyState";
|
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 {
|
interface VelocityBlockProps {
|
||||||
velocity: Velocity | null | undefined;
|
velocity: Velocity | null | undefined;
|
||||||
}
|
}
|
||||||
|
|
@ -121,6 +132,102 @@ export function VelocityBlock({ velocity }: VelocityBlockProps) {
|
||||||
({velocity.months_observed} мес)
|
({velocity.months_observed} мес)
|
||||||
</div>
|
</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 */}
|
{/* Top competitors */}
|
||||||
{velocity.sample_competitors.length > 0 && (
|
{velocity.sample_competitors.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -188,6 +295,19 @@ export function VelocityBlock({ velocity }: VelocityBlockProps) {
|
||||||
{c.obj_class}
|
{c.obj_class}
|
||||||
</span>
|
</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>
|
||||||
<td
|
<td
|
||||||
style={{
|
style={{
|
||||||
|
|
|
||||||
|
|
@ -205,6 +205,12 @@ export interface Pipeline24mo {
|
||||||
}
|
}
|
||||||
|
|
||||||
// D2 (#34) — velocity-score: темп продаж конкурентов
|
// D2 (#34) — velocity-score: темп продаж конкурентов
|
||||||
|
export interface VelocityBucketStat {
|
||||||
|
units: number;
|
||||||
|
sqm: number;
|
||||||
|
complexes_count: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface VelocityCompetitor {
|
export interface VelocityCompetitor {
|
||||||
obj_id: number;
|
obj_id: number;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
|
|
@ -213,6 +219,7 @@ export interface VelocityCompetitor {
|
||||||
total_sqm_period: number;
|
total_sqm_period: number;
|
||||||
dev_name?: string | null;
|
dev_name?: string | null;
|
||||||
district_name?: string | null;
|
district_name?: string | null;
|
||||||
|
by_room_bucket?: Record<string, number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VelocityPeriod {
|
export interface VelocityPeriod {
|
||||||
|
|
@ -229,6 +236,7 @@ export interface Velocity {
|
||||||
months_observed: number;
|
months_observed: number;
|
||||||
period: VelocityPeriod;
|
period: VelocityPeriod;
|
||||||
sample_competitors: VelocityCompetitor[];
|
sample_competitors: VelocityCompetitor[];
|
||||||
|
by_room_bucket?: Record<string, VelocityBucketStat>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// G5 (#32) — Gate verdict: can_build_mkd
|
// G5 (#32) — Gate verdict: can_build_mkd
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue