'use client'; import dynamic from 'next/dynamic'; import { useEffect, useState } from 'react'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'; import { analyticsApi, type MarketReportDistrict, type HeatmapDataPoint, type DistrictStats, type PriceTrendPoint, } from '@/lib/analytics-api'; const DistrictBarChart = dynamic( () => import('@/components/charts/district-bar-chart').then((mod) => mod.DistrictBarChart), { ssr: false, loading: () =>
Đang tải biểu đồ...
}, ); const PriceTrendChart = dynamic( () => import('@/components/charts/price-trend-chart').then((mod) => mod.PriceTrendChart), { ssr: false, loading: () =>
Đang tải biểu đồ...
}, ); const CITIES = ['Ho Chi Minh', 'Ha Noi', 'Da Nang']; const CURRENT_PERIOD = '2026-Q1'; const TREND_PERIODS = ['2025-Q1', '2025-Q2', '2025-Q3', '2025-Q4', '2026-Q1']; function formatPrice(priceStr: string): string { const num = Number(priceStr); if (num >= 1_000_000_000) return `${(num / 1_000_000_000).toFixed(1)} tỷ`; if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(0)} triệu`; return num.toLocaleString('vi-VN'); } function formatPriceM2(price: number): string { if (price >= 1_000_000) return `${(price / 1_000_000).toFixed(1)} tr/m²`; return `${price.toLocaleString('vi-VN')} đ/m²`; } function YoYBadge({ value }: { value: number | null }) { if (value === null) return N/A; const isPositive = value >= 0; return ( {isPositive ? '+' : ''} {value.toFixed(1)}% ); } export default function AnalyticsPage() { const [city, setCity] = useState(CITIES[0] ?? 'Ho Chi Minh'); const [period] = useState(CURRENT_PERIOD); const [tab, setTab] = useState('overview'); const [marketReport, setMarketReport] = useState([]); const [heatmap, setHeatmap] = useState([]); const [districtStats, setDistrictStats] = useState([]); const [priceTrend, setPriceTrend] = useState([]); const [trendDistrict, setTrendDistrict] = useState(''); const [loading, setLoading] = useState(true); const [trendLoading, setTrendLoading] = useState(false); const [error, setError] = useState(null); useEffect(() => { setLoading(true); setError(null); Promise.all([ analyticsApi .getMarketReport(city, period) .catch(() => ({ districts: [] as MarketReportDistrict[] })), analyticsApi .getHeatmap(city, period) .catch(() => ({ dataPoints: [] as HeatmapDataPoint[] })), analyticsApi .getDistrictStats(city, period) .catch(() => ({ districts: [] as DistrictStats[] })), ]) .then(([report, heatmapData, stats]) => { setMarketReport(report.districts); setHeatmap(heatmapData.dataPoints); setDistrictStats(stats.districts); // Auto-select first district for trend const firstDistrict = report.districts[0]?.district ?? ''; if (firstDistrict && !trendDistrict) { setTrendDistrict(firstDistrict); } }) .catch(() => setError('Không thể tải dữ liệu phân tích')) .finally(() => setLoading(false)); }, [city, period]); // Load price trend when district changes useEffect(() => { if (!trendDistrict || !city) return; setTrendLoading(true); analyticsApi .getPriceTrend(trendDistrict, city, 'APARTMENT', TREND_PERIODS) .then((res) => setPriceTrend(res.trend)) .catch(() => setPriceTrend([])) .finally(() => setTrendLoading(false)); }, [trendDistrict, city]); const totalListings = marketReport.reduce((sum, d) => sum + d.totalListings, 0); const avgDaysOnMarket = marketReport.length > 0 ? marketReport.reduce((sum, d) => sum + d.daysOnMarket, 0) / marketReport.length : 0; const avgPriceM2 = marketReport.length > 0 ? marketReport.reduce((sum, d) => sum + d.avgPriceM2, 0) / marketReport.length : 0; const uniqueDistricts = [...new Set(marketReport.map((d) => d.district))]; // Chart data for bar chart const barChartData = heatmap .sort((a, b) => b.avgPriceM2 - a.avgPriceM2) .map((p) => ({ district: p.district.replace(/^Quan\s*/i, 'Q.').replace(/^Huyen\s*/i, 'H.'), price: Math.round(p.avgPriceM2 / 1_000_000), listings: p.totalListings, })); // Chart data for line chart const trendChartData = priceTrend.map((p) => ({ period: p.period, 'Gia/m2': Math.round(p.avgPriceM2 / 1_000_000), 'Tin đăng': p.totalListings, })); return (

Phân tích thị trường

Báo cáo thị trường bất động sản - {period}

{CITIES.map((c) => ( ))}
{error &&
{error}
} {/* Summary Cards */}
Tổng tin đăng {loading ? '...' : totalListings.toLocaleString('vi-VN')} Giá TB/m² {loading ? '...' : formatPriceM2(avgPriceM2)} Ngày trung bình để bán {loading ? '...' : `${avgDaysOnMarket.toFixed(0)} ngày`} Số quận/huyện {loading ? '...' : new Set(marketReport.map((d) => d.district)).size}
{/* Tabs */} Tổng quan Xu hướng giá Chi tiết quận {/* Overview Tab */}
{/* Bar Chart - Price by District */} Giá trung bình theo quận Triệu VND/m² tại {city} {loading ? (
Đang tải...
) : barChartData.length === 0 ? (
Chưa có dữ liệu
) : ( )}
{/* Heatmap - Card Grid */} Bản đồ giá theo quận So sánh giá trung bình/m² tại {city} {loading ? (
Đang tải...
) : heatmap.length === 0 ? (
Chưa có dữ liệu
) : (
{heatmap .sort((a, b) => b.avgPriceM2 - a.avgPriceM2) .slice(0, 8) .map((point) => { const maxPrice = Math.max(...heatmap.map((h) => h.avgPriceM2)); const intensity = Math.round((point.avgPriceM2 / maxPrice) * 100); return (
{ setTrendDistrict(point.district); setTab('trends'); }} >
{point.district}
{formatPriceM2(point.avgPriceM2)}
{point.totalListings} tin đăng
); })}
)}
{/* Trends Tab */}
{/* District selector */}
{uniqueDistricts.map((d) => ( ))}
Xu hướng giá - {trendDistrict || 'Chọn quận'} Biến động giá trung bình/m² qua các quý (Căn hộ) {trendLoading ? (
Đang tải...
) : trendChartData.length === 0 ? (
Chưa có dữ liệu xu hướng
) : ( )}
{/* District Stats Tab */}
{/* Stats Table */} Thống kê chi tiết theo quận Dữ liệu thị trường bất động sản tại {city} - {period} {loading ? (
Đang tải...
) : districtStats.length === 0 ? (
Chưa có dữ liệu
) : (
{districtStats.map((stat, i) => ( ))}
Quận Loại BĐS Giá trung vị Giá/m² Tin đăng Ngày bán YoY
{stat.district} {stat.propertyType} {formatPrice(stat.medianPrice)} {formatPriceM2(stat.avgPriceM2)} {stat.totalListings} {stat.daysOnMarket.toFixed(0)}
)}
{/* Market Report Cards */} Báo cáo thị trường Tổng hợp chỉ số thị trường theo từng quận {loading ? (
Đang tải...
) : marketReport.length === 0 ? (
Chưa có dữ liệu
) : (
{[...new Map(marketReport.map((d) => [d.district, d])).values()].map( (district) => (

{district.district}

Giá trung vị {formatPrice(district.medianPrice)}
Giá/m² {formatPriceM2(district.avgPriceM2)}
Tin đăng {district.totalListings}
Tồn kho {district.inventoryLevel}
Thay đổi YoY
), )}
)}
); }