fix(sf-03): CompetitorTable Status column + filter tabs #278

Merged
lekss361 merged 1 commit from fix/sf-03-competitor-table-status into main 2026-05-17 10:50:27 +00:00
2 changed files with 135 additions and 4 deletions

View file

@ -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>

View file

@ -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 {