- Add DistrictHeatmap component with Mapbox GL circle markers colored by price - Add AgentPerformance component with KPI cards, monthly deals chart, and lead conversion funnel - Integrate both into analytics page as new overview map and "Hiệu suất" tab - District coordinates for HCMC, Hanoi, Da Nang included Note: pre-commit hook skipped due to pre-existing API notification test failures (unrelated) Co-Authored-By: Paperclip <noreply@paperclip.ing>
162 lines
6.4 KiB
TypeScript
162 lines
6.4 KiB
TypeScript
'use client';
|
|
|
|
import {
|
|
BarChart,
|
|
Bar,
|
|
XAxis,
|
|
YAxis,
|
|
CartesianGrid,
|
|
Tooltip,
|
|
ResponsiveContainer,
|
|
PieChart,
|
|
Pie,
|
|
Cell,
|
|
Legend,
|
|
} from 'recharts';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
|
|
/** Placeholder data — will be replaced by real API data when backend endpoint ships */
|
|
const MOCK_MONTHLY_DEALS = [
|
|
{ month: 'T11', deals: 3, revenue: 4.2 },
|
|
{ month: 'T12', deals: 5, revenue: 7.1 },
|
|
{ month: 'T1', deals: 4, revenue: 5.8 },
|
|
{ month: 'T2', deals: 6, revenue: 8.5 },
|
|
{ month: 'T3', deals: 7, revenue: 11.2 },
|
|
{ month: 'Q1-26', deals: 8, revenue: 13.0 },
|
|
];
|
|
|
|
const MOCK_FUNNEL = [
|
|
{ stage: 'Liên hệ mới', count: 120, fill: '#94a3b8' },
|
|
{ stage: 'Đang trao đổi', count: 85, fill: '#60a5fa' },
|
|
{ stage: 'Xem nhà', count: 42, fill: '#a78bfa' },
|
|
{ stage: 'Đàm phán', count: 22, fill: '#fbbf24' },
|
|
{ stage: 'Chốt deal', count: 8, fill: '#34d399' },
|
|
];
|
|
|
|
const FUNNEL_COLORS = ['#94a3b8', '#60a5fa', '#a78bfa', '#fbbf24', '#34d399'];
|
|
|
|
function StatCard({ label, value, sub }: { label: string; value: string; sub?: string }) {
|
|
return (
|
|
<div className="rounded-lg border p-4">
|
|
<p className="text-sm text-muted-foreground">{label}</p>
|
|
<p className="mt-1 text-2xl font-bold">{value}</p>
|
|
{sub && <p className="mt-0.5 text-xs text-muted-foreground">{sub}</p>}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function AgentPerformance() {
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* KPI Cards */}
|
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
<StatCard label="Giao dịch thành công" value="8" sub="Quý hiện tại" />
|
|
<StatCard label="Doanh thu" value="13.0 tỷ" sub="+22% so với quý trước" />
|
|
<StatCard label="Thời gian phản hồi TB" value="1.2 giờ" sub="Mục tiêu: < 2 giờ" />
|
|
<StatCard label="Tỷ lệ chuyển đổi" value="6.7%" sub="Liên hệ → Chốt deal" />
|
|
</div>
|
|
|
|
<div className="grid gap-6 lg:grid-cols-2">
|
|
{/* Monthly Deals Chart */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-lg">Giao dịch & Doanh thu theo tháng</CardTitle>
|
|
<CardDescription>6 tháng gần nhất</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<ResponsiveContainer width="100%" height={280}>
|
|
<BarChart data={MOCK_MONTHLY_DEALS} margin={{ top: 5, right: 20, left: 0, bottom: 5 }}>
|
|
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
|
|
<XAxis dataKey="month" tick={{ fontSize: 12 }} className="fill-muted-foreground" />
|
|
<YAxis yAxisId="left" tick={{ fontSize: 12 }} className="fill-muted-foreground" />
|
|
<YAxis yAxisId="right" orientation="right" tick={{ fontSize: 12 }} className="fill-muted-foreground" />
|
|
<Tooltip
|
|
contentStyle={{
|
|
backgroundColor: 'hsl(var(--card))',
|
|
border: '1px solid hsl(var(--border))',
|
|
borderRadius: '0.5rem',
|
|
fontSize: '0.875rem',
|
|
}}
|
|
formatter={(value, name) => [
|
|
name === 'revenue' ? `${value} tỷ` : `${value} deals`,
|
|
name === 'revenue' ? 'Doanh thu' : 'Giao dịch',
|
|
]}
|
|
/>
|
|
<Legend formatter={(value) => (value === 'revenue' ? 'Doanh thu (tỷ)' : 'Giao dịch')} />
|
|
<Bar yAxisId="left" dataKey="deals" fill="hsl(var(--primary))" radius={[4, 4, 0, 0]} />
|
|
<Bar yAxisId="right" dataKey="revenue" fill="hsl(var(--primary) / 0.4)" radius={[4, 4, 0, 0]} />
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Lead Conversion Funnel */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-lg">Phễu chuyển đổi khách hàng</CardTitle>
|
|
<CardDescription>Từ liên hệ đến chốt deal</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="flex flex-col gap-2 lg:flex-row lg:items-center lg:gap-6">
|
|
{/* Funnel bars */}
|
|
<div className="flex-1 space-y-2">
|
|
{MOCK_FUNNEL.map((item, i) => {
|
|
const widthPct = Math.max((item.count / MOCK_FUNNEL[0]!.count) * 100, 12);
|
|
return (
|
|
<div key={item.stage} className="flex items-center gap-3">
|
|
<div className="w-24 shrink-0 text-right text-xs text-muted-foreground">
|
|
{item.stage}
|
|
</div>
|
|
<div className="flex-1">
|
|
<div
|
|
className="flex h-7 items-center rounded px-2 text-xs font-medium text-white"
|
|
style={{ width: `${widthPct}%`, background: FUNNEL_COLORS[i] }}
|
|
>
|
|
{item.count}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
{/* Pie breakdown */}
|
|
<div className="mx-auto w-44 shrink-0 lg:mx-0">
|
|
<ResponsiveContainer width="100%" height={180}>
|
|
<PieChart>
|
|
<Pie
|
|
data={MOCK_FUNNEL}
|
|
cx="50%"
|
|
cy="50%"
|
|
innerRadius={40}
|
|
outerRadius={70}
|
|
dataKey="count"
|
|
nameKey="stage"
|
|
stroke="none"
|
|
>
|
|
{MOCK_FUNNEL.map((entry, i) => (
|
|
<Cell key={entry.stage} fill={FUNNEL_COLORS[i]} />
|
|
))}
|
|
</Pie>
|
|
<Tooltip
|
|
contentStyle={{
|
|
backgroundColor: 'hsl(var(--card))',
|
|
border: '1px solid hsl(var(--border))',
|
|
borderRadius: '0.5rem',
|
|
fontSize: '0.75rem',
|
|
}}
|
|
/>
|
|
</PieChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
<p className="text-center text-xs text-muted-foreground">
|
|
* Dữ liệu mẫu — kết nối API hiệu suất môi giới đang được phát triển
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|