fix(sf-03): CompetitorTable Status column + filter tabs
После backend PR #276 (site_status в /analyze) — frontend показывает статус строящиеся/сданные с цветным badge и фильтр-tabs наверху таблицы. TS type extended manually (backward compat) — codegen подхватит после backend deploy. Closes (epic part) #271 item 3
This commit is contained in:
parent
ae0fce3528
commit
98323027aa
2 changed files with 135 additions and 4 deletions
|
|
@ -1,7 +1,10 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import type { ParcelAnalysisCompetitor } from "@/types/site-finder";
|
||||
|
||||
type StatusFilter = "all" | "building" | "ready";
|
||||
|
||||
interface Props {
|
||||
competitors: ParcelAnalysisCompetitor[];
|
||||
districtName: string | null | undefined;
|
||||
|
|
@ -15,8 +18,94 @@ const CLASS_COLORS: Record<string, string> = {
|
|||
Премиум: "#b45309",
|
||||
};
|
||||
|
||||
function StatusBadge({ status }: { status: string | null | undefined }) {
|
||||
if (status === "Строящиеся") {
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
padding: "2px 6px",
|
||||
borderRadius: 4,
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
background: "#d1fae5",
|
||||
color: "#065f46",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
Строящиеся
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (status === "Сданные") {
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
padding: "2px 6px",
|
||||
borderRadius: 4,
|
||||
fontSize: 11,
|
||||
fontWeight: 500,
|
||||
background: "#f1f5f9",
|
||||
color: "#475569",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
Сданные
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
padding: "2px 6px",
|
||||
borderRadius: 4,
|
||||
fontSize: 11,
|
||||
background: "#f4f4f5",
|
||||
color: "#71717a",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
—
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function CompetitorTable({ competitors, districtName }: Props) {
|
||||
const top20 = competitors.slice(0, 20);
|
||||
const [filter, setFilter] = useState<StatusFilter>("all");
|
||||
|
||||
const buildingCount = competitors.filter(
|
||||
(c) => c.site_status === "Строящиеся",
|
||||
).length;
|
||||
const readyCount = competitors.filter(
|
||||
(c) => c.site_status === "Сданные",
|
||||
).length;
|
||||
|
||||
const filtered = competitors.filter((c) => {
|
||||
if (filter === "building") return c.site_status === "Строящиеся";
|
||||
if (filter === "ready") return c.site_status === "Сданные";
|
||||
return true;
|
||||
});
|
||||
|
||||
const top20 = filtered.slice(0, 20);
|
||||
|
||||
const tabBase = {
|
||||
padding: "4px 12px",
|
||||
borderRadius: 6,
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
cursor: "pointer",
|
||||
border: "1px solid transparent",
|
||||
background: "transparent",
|
||||
color: "#6b7280",
|
||||
transition: "all 0.15s",
|
||||
} as const;
|
||||
|
||||
const tabActive = {
|
||||
...tabBase,
|
||||
background: "#fff",
|
||||
border: "1px solid #d1d5db",
|
||||
color: "#111827",
|
||||
fontWeight: 600,
|
||||
} as const;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -27,6 +116,7 @@ export function CompetitorTable({ competitors, districtName }: Props) {
|
|||
background: "#fff",
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<div
|
||||
style={{
|
||||
padding: "14px 20px",
|
||||
|
|
@ -47,9 +137,42 @@ export function CompetitorTable({ competitors, districtName }: Props) {
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* Filter tabs */}
|
||||
<div
|
||||
style={{
|
||||
padding: "8px 16px",
|
||||
borderBottom: "1px solid #e5e7eb",
|
||||
background: "#f9fafb",
|
||||
display: "flex",
|
||||
gap: 6,
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<button
|
||||
style={filter === "all" ? tabActive : tabBase}
|
||||
onClick={() => setFilter("all")}
|
||||
>
|
||||
Все ({competitors.length})
|
||||
</button>
|
||||
<button
|
||||
style={filter === "building" ? tabActive : tabBase}
|
||||
onClick={() => setFilter("building")}
|
||||
>
|
||||
Строящиеся ({buildingCount})
|
||||
</button>
|
||||
<button
|
||||
style={filter === "ready" ? tabActive : tabBase}
|
||||
onClick={() => setFilter("ready")}
|
||||
>
|
||||
Сданные ({readyCount})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{top20.length === 0 ? (
|
||||
<div style={{ padding: 24, color: "#9ca3af", fontSize: 13 }}>
|
||||
Конкурентов не найдено
|
||||
{filtered.length === 0 && competitors.length > 0
|
||||
? "Нет конкурентов по выбранному фильтру"
|
||||
: "Конкурентов не найдено"}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ overflowX: "auto" }}>
|
||||
|
|
@ -66,6 +189,7 @@ export function CompetitorTable({ competitors, districtName }: Props) {
|
|||
"ЖК",
|
||||
"Девелопер",
|
||||
"Класс",
|
||||
"Статус",
|
||||
"Квартир",
|
||||
"Район",
|
||||
"Расст., м",
|
||||
|
|
@ -145,6 +269,9 @@ export function CompetitorTable({ competitors, districtName }: Props) {
|
|||
<span style={{ color: "#d1d5db" }}>—</span>
|
||||
)}
|
||||
</td>
|
||||
<td style={{ padding: "7px 12px" }}>
|
||||
<StatusBadge status={c.site_status} />
|
||||
</td>
|
||||
<td
|
||||
style={{
|
||||
padding: "7px 12px",
|
||||
|
|
@ -176,7 +303,7 @@ export function CompetitorTable({ competitors, districtName }: Props) {
|
|||
</table>
|
||||
</div>
|
||||
)}
|
||||
{competitors.length > 20 && (
|
||||
{filtered.length > 20 && (
|
||||
<div
|
||||
style={{
|
||||
padding: "8px 20px",
|
||||
|
|
@ -186,7 +313,7 @@ export function CompetitorTable({ competitors, districtName }: Props) {
|
|||
color: "#9ca3af",
|
||||
}}
|
||||
>
|
||||
Показано 20 из {competitors.length} ближайших ЖК
|
||||
Показано 20 из {filtered.length} ближайших ЖК
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -39,6 +39,10 @@ export interface ParcelAnalysisCompetitor {
|
|||
flat_count: number | null;
|
||||
district_name: string | null;
|
||||
distance_m: number;
|
||||
// Added manually — backend PR #276 (site_status in /analyze).
|
||||
// Codegen will overwrite after backend deploy (additive, backward compat).
|
||||
site_status?: string | null; // 'Строящиеся' | 'Сданные' | null
|
||||
ready_dt?: string | null; // ISO date
|
||||
}
|
||||
|
||||
export interface ParcelAnalysisDistrict {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue