'use client'; import { BarChart3, Building2, Layers, TrendingUp } from 'lucide-react'; import * as React from 'react'; import { DistrictHeatmap } from '@/components/charts/district-heatmap'; import { PriceAreaChart } from '@/components/charts/price-area-chart'; import { DataTable } from '@/components/design-system/data-table'; import type { DataTableColumn } from '@/components/design-system/data-table'; import { MarketIndex } from '@/components/design-system/market-index'; import { PriceDelta } from '@/components/design-system/price-delta'; import { StatCard } from '@/components/design-system/stat-card'; import { useDistrictStats, useHeatmap } from '@/lib/hooks/use-analytics'; import { listingsApi } from '@/lib/listings-api'; /* ------------------------------------------------------------------ */ /* Helpers */ /* ------------------------------------------------------------------ */ function formatTr(value: number): string { if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}`; return `${Math.round(value / 1000)}k`; } /** Generate current period key (YYYY-MM). */ function currentPeriod(): string { const now = new Date(); return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`; } /* ------------------------------------------------------------------ */ /* Types for the district table */ /* ------------------------------------------------------------------ */ interface DistrictRow { district: string; avgPriceM2: number; yoyChange: number | null; totalListings: number; daysOnMarket: number; } /* ------------------------------------------------------------------ */ /* Page */ /* ------------------------------------------------------------------ */ export default function MarketDashboardPage() { const city = 'Ho Chi Minh'; const period = currentPeriod(); /* --- Data hooks --- */ const { data: districtData, isLoading: districtLoading } = useDistrictStats(city, period); const { data: heatmapData, isLoading: heatmapLoading } = useHeatmap(city, period); /* --- Listings count (lightweight) --- */ const [totalListings, setTotalListings] = React.useState(null); React.useEffect(() => { listingsApi .search({ limit: 1, status: 'ACTIVE' }) .then((res) => setTotalListings(res.total ?? res.data.length)) .catch(() => {}); }, []); /* --- Derived stats --- */ const districts: DistrictRow[] = React.useMemo(() => { if (!districtData?.districts) return []; return districtData.districts.map((d) => ({ district: d.district, avgPriceM2: d.avgPriceM2, yoyChange: d.yoyChange, totalListings: d.totalListings, daysOnMarket: d.daysOnMarket, })); }, [districtData]); const avgPriceM2 = React.useMemo(() => { if (districts.length === 0) return 0; return districts.reduce((s, d) => s + d.avgPriceM2, 0) / districts.length; }, [districts]); const avgChange7d = React.useMemo(() => { const withChange = districts.filter((d) => d.yoyChange != null); if (withChange.length === 0) return 0; return withChange.reduce((s, d) => s + (d.yoyChange ?? 0), 0) / withChange.length; }, [districts]); const totalTransactions = React.useMemo( () => districts.reduce((s, d) => s + d.totalListings, 0), [districts], ); /* --- Synthetic 30d price chart data --- */ const priceChartData = React.useMemo(() => { if (districts.length === 0) return []; const base = avgPriceM2; return Array.from({ length: 30 }, (_, i) => ({ period: `D${i + 1}`, avgPriceM2: base * (0.97 + Math.random() * 0.06), })); // eslint-disable-next-line react-hooks/exhaustive-deps }, [districts.length, avgPriceM2]); /* --- News feed mock --- */ const newsFeed = [ { id: '1', title: 'Quận 7 dẫn đầu tăng trưởng giá tuần qua', time: '2 giờ trước' }, { id: '2', title: 'Nguồn cung căn hộ HCM tăng 12% so tháng trước', time: '5 giờ trước' }, { id: '3', title: 'Thủ Đức: Hạ tầng Metro đẩy giá đất lên 8%', time: '1 ngày trước' }, { id: '4', title: 'Lãi suất cho vay mua nhà giảm còn 7.5%/năm', time: '2 ngày trước' }, ]; /* --- Table columns --- */ const tableColumns: DataTableColumn[] = React.useMemo( () => [ { id: 'district', header: 'Quận', cell: (r) => {r.district}, sortable: true, sortValue: (r) => r.district, }, { id: 'price', header: 'Giá TB/m²', cell: (r) => `${formatTr(r.avgPriceM2)} tr`, align: 'right' as const, numeric: true, sortable: true, sortValue: (r) => r.avgPriceM2, }, { id: 'change', header: 'Δ7d', cell: (r) => r.yoyChange != null ? ( ) : ( ), align: 'right' as const, numeric: true, sortable: true, sortValue: (r) => r.yoyChange ?? 0, }, { id: 'volume', header: 'Vol', cell: (r) => r.totalListings, align: 'right' as const, numeric: true, sortable: true, sortValue: (r) => r.totalListings, }, { id: 'dom', header: 'DT', cell: (r) => `${r.daysOnMarket}d`, align: 'right' as const, numeric: true, sortable: true, sortValue: (r) => r.daysOnMarket, }, ], [], ); /* --- GGX Market Index --- */ const ggxValue = avgPriceM2 > 0 ? formatTr(avgPriceM2) : '—'; return (
{/* 1. Hero: Market Index */}

Chỉ số thị trường BĐS TP. Hồ Chí Minh — cập nhật theo thời gian thực

{/* 2. Stat cards strip */}
} sublabel="đang hoạt động" /> } sublabel="trong kỳ" /> 0 ? formatTr(avgPriceM2) : '—'} unit="tr/m²" icon={} sublabel="toàn thành" /> 0 ? '+' : ''}${avgChange7d.toFixed(2)}%` : '—'} delta={avgChange7d || undefined} icon={} sublabel="7 ngày" />
{/* 3. Two-column grid: Table + Chart */}
{/* Left: District table */}

Top khu vực

r.district} emptyText="Chưa có dữ liệu khu vực" />
{/* Right: 30d price area chart */}

Biểu đồ giá 30 ngày

{priceChartData.length > 0 ? ( ) : (
{districtLoading ? 'Đang tải...' : 'Chưa có dữ liệu'}
)}
{/* 4. Bottom grid: Heatmap + News feed */}
{/* Heatmap — takes 2 cols */}

Bản đồ nhiệt giá

{heatmapLoading ? (
Đang tải bản đồ...
) : ( )}
{/* News feed compact */}

Tin tức thị trường

    {newsFeed.map((item) => (
  • {item.title}

    {item.time}

  • ))}
); }