'use client'; import { useEffect, useState } from 'react'; import { BarChart, Bar, LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend, } from 'recharts'; 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 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)} ty`; if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(0)} trieu`; return num.toLocaleString('vi-VN'); } function formatPriceM2(price: number): string { if (price >= 1_000_000) return `${(price / 1_000_000).toFixed(1)} tr/m2`; return `${price.toLocaleString('vi-VN')} d/m2`; } 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('Khong the tai du lieu phan tich')) .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 dang': p.totalListings, })); return (

Phan tich thi truong

Bao cao thi truong bat dong san - {period}

{CITIES.map((c) => ( ))}
{error &&
{error}
} {/* Summary Cards */}
Tong tin dang {loading ? '...' : totalListings.toLocaleString('vi-VN')} Gia TB/m2 {loading ? '...' : formatPriceM2(avgPriceM2)} Ngay trung binh de ban {loading ? '...' : `${avgDaysOnMarket.toFixed(0)} ngay`} So quan/huyen {loading ? '...' : new Set(marketReport.map((d) => d.district)).size}
{/* Tabs */} Tong quan Xu huong gia Chi tiet quan {/* Overview Tab */}
{/* Bar Chart - Price by District */} Gia trung binh theo quan Trieu VND/m2 tai {city} {loading ? (
Dang tai...
) : barChartData.length === 0 ? (
Chua co du lieu
) : ( [ name === 'price' ? `${value} tr/m2` : value, name === 'price' ? 'Gia' : 'Tin dang', ]} /> )}
{/* Heatmap - Card Grid */} Ban do gia theo quan So sanh gia trung binh/m2 tai {city} {loading ? (
Dang tai...
) : heatmap.length === 0 ? (
Chua co du lieu
) : (
{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 dang
); })}
)}
{/* Trends Tab */}
{/* District selector */}
{uniqueDistricts.map((d) => ( ))}
Xu huong gia - {trendDistrict || 'Chon quan'} Bien dong gia trung binh/m2 qua cac quy (Can ho) {trendLoading ? (
Dang tai...
) : trendChartData.length === 0 ? (
Chua co du lieu xu huong
) : ( [ name === 'Gia/m2' ? `${value} tr/m2` : value, name, ]} /> )}
{/* District Stats Tab */}
{/* Stats Table */} Thong ke chi tiet theo quan Du lieu thi truong bat dong san tai {city} - {period} {loading ? (
Dang tai...
) : districtStats.length === 0 ? (
Chua co du lieu
) : (
{districtStats.map((stat, i) => ( ))}
Quan Loai BDS Gia trung vi Gia/m2 Tin dang Ngay ban YoY
{stat.district} {stat.propertyType} {formatPrice(stat.medianPrice)} {formatPriceM2(stat.avgPriceM2)} {stat.totalListings} {stat.daysOnMarket.toFixed(0)}
)}
{/* Market Report Cards */} Bao cao thi truong Tong hop chi so thi truong theo tung quan {loading ? (
Dang tai...
) : marketReport.length === 0 ? (
Chua co du lieu
) : (
{[...new Map(marketReport.map((d) => [d.district, d])).values()].map( (district) => (

{district.district}

Gia trung vi {formatPrice(district.medianPrice)}
Gia/m2 {formatPriceM2(district.avgPriceM2)}
Tin dang {district.totalListings}
Ton kho {district.inventoryLevel}
Thay doi YoY
), )}
)}
); }