'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 { useMarketReport, useHeatmap, useDistrictStats, usePriceTrend, } from '@/lib/hooks/use-analytics'; 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 DistrictHeatmap = dynamic( () => import('@/components/charts/district-heatmap').then((mod) => mod.DistrictHeatmap), { ssr: false, loading: () =>
Đang tải bản đồ nhiệt...
}, ); const AgentPerformance = dynamic( () => import('@/components/charts/agent-performance').then((mod) => mod.AgentPerformance), { ssr: false, loading: () =>
Đang tải...
}, ); 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 = CURRENT_PERIOD; const [tab, setTab] = useState('overview'); const [trendDistrict, setTrendDistrict] = useState(''); const { data: reportData, isLoading: reportLoading, error: reportError } = useMarketReport(city, period); const { data: heatmapData, isLoading: heatmapLoading } = useHeatmap(city, period); const { data: statsData, isLoading: statsLoading } = useDistrictStats(city, period); const { data: trendData, isLoading: trendLoading } = usePriceTrend( trendDistrict, city, 'APARTMENT', TREND_PERIODS, ); const loading = reportLoading || heatmapLoading || statsLoading; const error = reportError ? 'Không thể tải dữ liệu phân tích' : null; const marketReport = reportData?.districts ?? []; const heatmap = heatmapData?.dataPoints ?? []; const districtStats = statsData?.districts ?? []; const priceTrend = trendData?.trend ?? []; // Auto-select first district for trend const firstDistrict = marketReport[0]?.district ?? ''; useEffect(() => { if (firstDistrict && !trendDistrict) { setTrendDistrict(firstDistrict); } }, [firstDistrict, trendDistrict]); 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 Hiệu suất {/* 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 - Mapbox Map */} Bản đồ nhiệt 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
) : ( { setTrendDistrict(district); setTab('trends'); }} /> )}
{/* 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
), )}
)}
{/* Agent Performance Tab */}
); }