feat(sf-fe-a8): Section 3.2 Планировки + 3.3 Остатки/Velocity + CompetitorTable drawer pattern
This commit is contained in:
parent
68b1968826
commit
41bc557ab2
2 changed files with 389 additions and 57 deletions
|
|
@ -8,6 +8,7 @@ type StatusFilter = "all" | "building" | "ready";
|
||||||
interface Props {
|
interface Props {
|
||||||
competitors: ParcelAnalysisCompetitor[];
|
competitors: ParcelAnalysisCompetitor[];
|
||||||
districtName: string | null | undefined;
|
districtName: string | null | undefined;
|
||||||
|
onRowClick?: (competitor: ParcelAnalysisCompetitor) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CLASS_COLORS: Record<string, string> = {
|
const CLASS_COLORS: Record<string, string> = {
|
||||||
|
|
@ -69,7 +70,11 @@ function StatusBadge({ status }: { status: string | null | undefined }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CompetitorTable({ competitors, districtName }: Props) {
|
export function CompetitorTable({
|
||||||
|
competitors,
|
||||||
|
districtName,
|
||||||
|
onRowClick,
|
||||||
|
}: Props) {
|
||||||
const [filter, setFilter] = useState<StatusFilter>("all");
|
const [filter, setFilter] = useState<StatusFilter>("all");
|
||||||
|
|
||||||
const buildingCount = competitors.filter(
|
const buildingCount = competitors.filter(
|
||||||
|
|
@ -193,7 +198,7 @@ export function CompetitorTable({ competitors, districtName }: Props) {
|
||||||
"Квартир",
|
"Квартир",
|
||||||
"Район",
|
"Район",
|
||||||
"Расст., м",
|
"Расст., м",
|
||||||
].map((h) => (
|
].map((h, idx) => (
|
||||||
<th
|
<th
|
||||||
key={h}
|
key={h}
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -203,6 +208,16 @@ export function CompetitorTable({ competitors, districtName }: Props) {
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
color: "#374151",
|
color: "#374151",
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: "nowrap",
|
||||||
|
// Sticky first column
|
||||||
|
...(idx === 0
|
||||||
|
? {
|
||||||
|
position: "sticky",
|
||||||
|
left: 0,
|
||||||
|
background: "#f6f7f9",
|
||||||
|
zIndex: 1,
|
||||||
|
boxShadow: "2px 0 4px rgba(0,0,0,0.04)",
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{h}
|
{h}
|
||||||
|
|
@ -214,16 +229,31 @@ export function CompetitorTable({ competitors, districtName }: Props) {
|
||||||
{top20.map((c, i) => {
|
{top20.map((c, i) => {
|
||||||
const sameDistrict =
|
const sameDistrict =
|
||||||
districtName && c.district_name === districtName;
|
districtName && c.district_name === districtName;
|
||||||
|
const rowBg = sameDistrict
|
||||||
|
? "#eff6ff"
|
||||||
|
: i % 2
|
||||||
|
? "#fafbfc"
|
||||||
|
: "#fff";
|
||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
key={c.obj_id}
|
key={c.obj_id}
|
||||||
|
onClick={() => onRowClick?.(c)}
|
||||||
style={{
|
style={{
|
||||||
background: sameDistrict
|
background: rowBg,
|
||||||
? "#eff6ff"
|
|
||||||
: i % 2
|
|
||||||
? "#fafbfc"
|
|
||||||
: "#fff",
|
|
||||||
borderBottom: "1px solid #f3f4f6",
|
borderBottom: "1px solid #f3f4f6",
|
||||||
|
cursor: onRowClick ? "pointer" : "default",
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
if (onRowClick) {
|
||||||
|
(
|
||||||
|
e.currentTarget as HTMLTableRowElement
|
||||||
|
).style.background = "#e0e7ff";
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
(
|
||||||
|
e.currentTarget as HTMLTableRowElement
|
||||||
|
).style.background = rowBg;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<td
|
<td
|
||||||
|
|
@ -235,6 +265,11 @@ export function CompetitorTable({ competitors, districtName }: Props) {
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
textOverflow: "ellipsis",
|
textOverflow: "ellipsis",
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: "nowrap",
|
||||||
|
// Sticky first column
|
||||||
|
position: "sticky",
|
||||||
|
left: 0,
|
||||||
|
background: rowBg,
|
||||||
|
boxShadow: "2px 0 4px rgba(0,0,0,0.04)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{c.comm_name ?? `ЖК #${c.obj_id}`}
|
{c.comm_name ?? `ЖК #${c.obj_id}`}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,24 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState, type ReactNode } from "react";
|
||||||
|
import { X, Building2, MapPin, Users, Ruler } from "lucide-react";
|
||||||
|
|
||||||
import { WeightProfilePanel } from "@/components/site-finder/WeightProfilePanel";
|
import { WeightProfilePanel } from "@/components/site-finder/WeightProfilePanel";
|
||||||
|
import { BestLayoutsBlock } from "@/components/site-finder/BestLayoutsBlock";
|
||||||
|
import { Pipeline24moBlock } from "@/components/site-finder/Pipeline24moBlock";
|
||||||
|
import { VelocityBlock } from "@/components/site-finder/VelocityBlock";
|
||||||
|
import { MarketTrendBlock } from "@/components/site-finder/MarketTrendBlock";
|
||||||
|
import { CompetitorTable } from "@/components/site-finder/CompetitorTable";
|
||||||
|
import { Drawer } from "@/components/ui/Drawer";
|
||||||
|
import { Badge } from "@/components/ui/Badge";
|
||||||
import {
|
import {
|
||||||
POI_DEFAULT_WEIGHTS,
|
POI_DEFAULT_WEIGHTS,
|
||||||
type PoiCategoryKey,
|
type PoiCategoryKey,
|
||||||
} from "@/lib/api/weightProfiles";
|
} from "@/lib/api/weightProfiles";
|
||||||
import type { ParcelAnalysis } from "@/types/site-finder";
|
import type {
|
||||||
|
ParcelAnalysis,
|
||||||
|
ParcelAnalysisCompetitor,
|
||||||
|
} from "@/types/site-finder";
|
||||||
|
|
||||||
// ── Types ─────────────────────────────────────────────────────────────────────
|
// ── Types ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -256,65 +267,333 @@ function Section31Settings({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Section 3.2 placeholder ────────────────────────────────────────────────────
|
// ── Section 3.2 «Планировки» ───────────────────────────────────────────────────
|
||||||
|
|
||||||
function Section32Placeholder() {
|
function Section32Layouts({ cad }: { cad: string }) {
|
||||||
return (
|
return (
|
||||||
<div id="section-3-2" style={{ scrollMarginTop: 72 }}>
|
<div id="section-3-2" style={{ scrollMarginTop: 72 }}>
|
||||||
<h3
|
<div style={{ marginBottom: 16 }}>
|
||||||
style={{
|
<h3
|
||||||
fontSize: 16,
|
style={{
|
||||||
fontWeight: 600,
|
fontSize: 16,
|
||||||
color: "var(--fg-primary, #111111)",
|
fontWeight: 600,
|
||||||
margin: "0 0 12px",
|
color: "var(--fg-primary, #111111)",
|
||||||
}}
|
margin: 0,
|
||||||
>
|
}}
|
||||||
3.2 Планировки конкурентов
|
>
|
||||||
</h3>
|
3.2 Планировки
|
||||||
<div
|
</h3>
|
||||||
style={{
|
<p
|
||||||
background: "var(--bg-card-alt, #FAFBFC)",
|
style={{
|
||||||
border: "1px dashed var(--border-strong, #D1D5DB)",
|
fontSize: 13,
|
||||||
borderRadius: 12,
|
color: "var(--fg-secondary, #5B6066)",
|
||||||
padding: "32px 24px",
|
margin: "4px 0 0",
|
||||||
textAlign: "center",
|
}}
|
||||||
color: "var(--fg-tertiary, #73767E)",
|
>
|
||||||
fontSize: 13,
|
Data-driven ТЗ на планировочный микс — на основе сделок конкурентов в
|
||||||
}}
|
радиусе
|
||||||
>
|
</p>
|
||||||
Раздел появится в A8 — планировки конкурентов (BestLayoutsBlock)
|
|
||||||
</div>
|
</div>
|
||||||
|
<BestLayoutsBlock cadNum={cad} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Section 3.3 placeholder ────────────────────────────────────────────────────
|
// ── Section 3.3 «Остатки и скорость» ──────────────────────────────────────────
|
||||||
|
|
||||||
|
function Section33RemainVelocity({ data }: { data: ParcelAnalysis }) {
|
||||||
|
const hasPipeline = data.pipeline_24mo != null;
|
||||||
|
const hasVelocity = data.velocity != null;
|
||||||
|
const hasTrend = data.market_trend != null;
|
||||||
|
|
||||||
|
const isEmpty = !hasPipeline && !hasVelocity && !hasTrend;
|
||||||
|
|
||||||
function Section33Placeholder() {
|
|
||||||
return (
|
return (
|
||||||
<div id="section-3-3" style={{ scrollMarginTop: 72 }}>
|
<div id="section-3-3" style={{ scrollMarginTop: 72 }}>
|
||||||
<h3
|
<div style={{ marginBottom: 16 }}>
|
||||||
style={{
|
<h3
|
||||||
fontSize: 16,
|
style={{
|
||||||
fontWeight: 600,
|
fontSize: 16,
|
||||||
color: "var(--fg-primary, #111111)",
|
fontWeight: 600,
|
||||||
margin: "0 0 12px",
|
color: "var(--fg-primary, #111111)",
|
||||||
}}
|
margin: 0,
|
||||||
>
|
}}
|
||||||
3.3 Остатки и скорость продаж
|
>
|
||||||
</h3>
|
3.3 Остатки и скорость
|
||||||
|
</h3>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
fontSize: 13,
|
||||||
|
color: "var(--fg-secondary, #5B6066)",
|
||||||
|
margin: "4px 0 0",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Pipeline конкурентов на 24 мес · темп продаж · тренд цен
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isEmpty ? (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: "var(--bg-card-alt, #FAFBFC)",
|
||||||
|
border: "1px solid var(--border-card, #E6E8EC)",
|
||||||
|
borderRadius: 12,
|
||||||
|
padding: "24px",
|
||||||
|
color: "var(--fg-tertiary, #73767E)",
|
||||||
|
fontSize: 13,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Данные по остаткам и скорости продаж отсутствуют для этого участка
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "repeat(auto-fit, minmax(280px, 1fr))",
|
||||||
|
gap: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{hasPipeline && <Pipeline24moBlock data={data.pipeline_24mo!} />}
|
||||||
|
{hasVelocity && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: "var(--bg-card, #FFFFFF)",
|
||||||
|
border: "1px solid var(--border-card, #E6E8EC)",
|
||||||
|
borderRadius: 10,
|
||||||
|
padding: "14px 18px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<VelocityBlock velocity={data.velocity} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{hasTrend && <MarketTrendBlock trend={data.market_trend} />}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Competitor detail drawer ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const STATUS_BADGE_MAP: Record<
|
||||||
|
string,
|
||||||
|
{ variant: "success" | "neutral" | "warning"; label: string }
|
||||||
|
> = {
|
||||||
|
Строящиеся: { variant: "warning", label: "Строящийся" },
|
||||||
|
Сданные: { variant: "success", label: "Сдан" },
|
||||||
|
};
|
||||||
|
|
||||||
|
function CompetitorDetailDrawer({
|
||||||
|
competitor,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
competitor: ParcelAnalysisCompetitor | null;
|
||||||
|
onClose: () => void;
|
||||||
|
}) {
|
||||||
|
const isOpen = competitor !== null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer open={isOpen} onClose={onClose} side="right" width="420px">
|
||||||
|
{competitor && (
|
||||||
|
<>
|
||||||
|
{/* Header */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: "16px 20px 12px",
|
||||||
|
borderBottom: "1px solid var(--border-soft, #EEF0F3)",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
gap: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Building2
|
||||||
|
size={16}
|
||||||
|
strokeWidth={1.5}
|
||||||
|
style={{
|
||||||
|
color: "var(--fg-secondary, #5B6066)",
|
||||||
|
marginTop: 3,
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "var(--fg-primary, #111111)",
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{competitor.comm_name ?? `ЖК #${competitor.obj_id}`}
|
||||||
|
</div>
|
||||||
|
{competitor.dev_name && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: 12,
|
||||||
|
color: "var(--fg-secondary, #5B6066)",
|
||||||
|
marginTop: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{competitor.dev_name}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
aria-label="Закрыть"
|
||||||
|
style={{
|
||||||
|
background: "none",
|
||||||
|
border: "none",
|
||||||
|
cursor: "pointer",
|
||||||
|
padding: 4,
|
||||||
|
color: "var(--fg-tertiary, #73767E)",
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<X size={18} strokeWidth={1.5} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status + badges row */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: "12px 20px",
|
||||||
|
borderBottom: "1px solid var(--border-soft, #EEF0F3)",
|
||||||
|
display: "flex",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
gap: 8,
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{competitor.site_status && (
|
||||||
|
<Badge
|
||||||
|
variant={
|
||||||
|
STATUS_BADGE_MAP[competitor.site_status]?.variant ?? "neutral"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{STATUS_BADGE_MAP[competitor.site_status]?.label ??
|
||||||
|
competitor.site_status}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{competitor.obj_class && (
|
||||||
|
<Badge variant="info">{competitor.obj_class}</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* KPI grid */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: "16px 20px",
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "1fr 1fr",
|
||||||
|
gap: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<KpiCell
|
||||||
|
icon={<MapPin size={14} strokeWidth={1.5} />}
|
||||||
|
label="Расстояние"
|
||||||
|
value={`${Math.round(competitor.distance_m).toLocaleString("ru-RU")} м`}
|
||||||
|
/>
|
||||||
|
<KpiCell
|
||||||
|
icon={<Users size={14} strokeWidth={1.5} />}
|
||||||
|
label="Квартир"
|
||||||
|
value={
|
||||||
|
competitor.flat_count != null
|
||||||
|
? competitor.flat_count.toLocaleString("ru-RU")
|
||||||
|
: "—"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{competitor.district_name && (
|
||||||
|
<KpiCell
|
||||||
|
icon={<MapPin size={14} strokeWidth={1.5} />}
|
||||||
|
label="Район"
|
||||||
|
value={competitor.district_name}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{competitor.ready_dt && (
|
||||||
|
<KpiCell
|
||||||
|
icon={<Ruler size={14} strokeWidth={1.5} />}
|
||||||
|
label="Срок сдачи"
|
||||||
|
value={new Date(competitor.ready_dt).toLocaleDateString(
|
||||||
|
"ru-RU",
|
||||||
|
{
|
||||||
|
year: "numeric",
|
||||||
|
month: "2-digit",
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Caveat */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
margin: "0 20px 20px",
|
||||||
|
padding: "10px 14px",
|
||||||
|
background: "var(--bg-card-alt, #FAFBFC)",
|
||||||
|
border: "1px solid var(--border-soft, #EEF0F3)",
|
||||||
|
borderRadius: 8,
|
||||||
|
fontSize: 12,
|
||||||
|
color: "var(--fg-tertiary, #73767E)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Детальные данные (sold%, цены, план. продажи) появятся после
|
||||||
|
расширения B5
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function KpiCell({
|
||||||
|
icon,
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
}: {
|
||||||
|
icon: ReactNode;
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: "var(--bg-card-alt, #FAFBFC)",
|
||||||
|
border: "1px solid var(--border-card, #E6E8EC)",
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: "10px 12px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
background: "var(--bg-card-alt, #FAFBFC)",
|
display: "flex",
|
||||||
border: "1px dashed var(--border-strong, #D1D5DB)",
|
alignItems: "center",
|
||||||
borderRadius: 12,
|
gap: 4,
|
||||||
padding: "32px 24px",
|
fontSize: 11,
|
||||||
textAlign: "center",
|
fontWeight: 500,
|
||||||
color: "var(--fg-tertiary, #73767E)",
|
textTransform: "uppercase" as const,
|
||||||
fontSize: 13,
|
letterSpacing: "0.04em",
|
||||||
|
color: "var(--fg-secondary, #5B6066)",
|
||||||
|
marginBottom: 4,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Раздел появится в A8 — Pipeline24mo / Velocity / MarketTrend
|
<span style={{ display: "flex", color: "var(--fg-tertiary, #73767E)" }}>
|
||||||
|
{icon}
|
||||||
|
</span>
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "var(--fg-primary, #111111)",
|
||||||
|
fontVariantNumeric: "tabular-nums",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -348,8 +627,11 @@ export function Section3SettingsAndCompetitors({ cad, data }: Props) {
|
||||||
maxAge6mo: false,
|
maxAge6mo: false,
|
||||||
onlyApartments: false,
|
onlyApartments: false,
|
||||||
});
|
});
|
||||||
|
const [selectedCompetitor, setSelectedCompetitor] =
|
||||||
|
useState<ParcelAnalysisCompetitor | null>(null);
|
||||||
|
|
||||||
const filteredCompetitors = applyFilters(data, filters);
|
const filteredCompetitors = applyFilters(data, filters);
|
||||||
|
const districtName = data.district?.district_name ?? null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id="section-3" style={{ scrollMarginTop: 72 }}>
|
<section id="section-3" style={{ scrollMarginTop: 72 }}>
|
||||||
|
|
@ -387,8 +669,18 @@ export function Section3SettingsAndCompetitors({ cad, data }: Props) {
|
||||||
{/* Sub-sections */}
|
{/* Sub-sections */}
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: 24 }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: 24 }}>
|
||||||
<Section31Settings filters={filters} onFiltersChange={setFilters} />
|
<Section31Settings filters={filters} onFiltersChange={setFilters} />
|
||||||
<Section32Placeholder />
|
|
||||||
<Section33Placeholder />
|
{/* Competitor table — moved before 3.2/3.3 for context */}
|
||||||
|
{filteredCompetitors.length > 0 && (
|
||||||
|
<CompetitorTable
|
||||||
|
competitors={filteredCompetitors}
|
||||||
|
districtName={districtName}
|
||||||
|
onRowClick={setSelectedCompetitor}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Section32Layouts cad={cad} />
|
||||||
|
<Section33RemainVelocity data={data} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Filtered competitors count hint */}
|
{/* Filtered competitors count hint */}
|
||||||
|
|
@ -405,10 +697,15 @@ export function Section3SettingsAndCompetitors({ cad, data }: Props) {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Фильтр по радиусу: показано {filteredCompetitors.length} из{" "}
|
Фильтр по радиусу: показано {filteredCompetitors.length} из{" "}
|
||||||
{data.competitors.length} конкурентов. Разделы 3.2 и 3.3 отобразят
|
{data.competitors.length} конкурентов
|
||||||
данные с учётом выборки после A8.
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Competitor detail drawer */}
|
||||||
|
<CompetitorDetailDrawer
|
||||||
|
competitor={selectedCompetitor}
|
||||||
|
onClose={() => setSelectedCompetitor(null)}
|
||||||
|
/>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue