'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: () =>
Dang tai bieu do...
},
);
const PriceTrendChart = dynamic(
() => import('@/components/charts/price-trend-chart').then((mod) => mod.PriceTrendChart),
{ ssr: false, loading: () => Dang tai bieu do...
},
);
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
) : (
)}
{/* 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
) : (
)}
{/* 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
) : (
| Quan |
Loai BDS |
Gia trung vi |
Gia/m2 |
Tin dang |
Ngay ban |
YoY |
{districtStats.map((stat, i) => (
| {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
),
)}
)}
);
}