150 lines
5.2 KiB
TypeScript
150 lines
5.2 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { orderBookingClient } from '../api/clients';
|
|
import { ORDER_BOOKING } from '../api/config';
|
|
import { Spinner } from '../components/reusable/Spinner';
|
|
import { EmptyState } from '../components/reusable/EmptyState';
|
|
import { StatsTiles } from '../components/reusable/StatsTiles';
|
|
import { TrendingUp } from 'lucide-react';
|
|
import {
|
|
BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, ResponsiveContainer,
|
|
PieChart, Pie, Cell, Legend
|
|
} from 'recharts';
|
|
|
|
interface Tile {
|
|
tile_uid: string;
|
|
key: string;
|
|
value: string | number;
|
|
}
|
|
|
|
interface ChartRow {
|
|
dimension: string | null;
|
|
value: number;
|
|
}
|
|
|
|
interface ChartData {
|
|
chart_uid: string;
|
|
key: string;
|
|
rows: ChartRow[];
|
|
}
|
|
|
|
const COLORS = ['#6366f1', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#06b6d4', '#f97316'];
|
|
|
|
export function AnalyticsPage() {
|
|
const [tiles, setTiles] = useState<Tile[]>([]);
|
|
const [charts, setCharts] = useState<ChartData[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
let live = true;
|
|
async function run() {
|
|
try {
|
|
setLoading(true);
|
|
const res = await orderBookingClient.recordView(ORDER_BOOKING.recordViews.ANALYTICS, {
|
|
limit: 100,
|
|
sortBy: 'instance_id',
|
|
sortDir: 'desc'
|
|
});
|
|
|
|
if (live) {
|
|
setTiles((res.tile_values || []) as Tile[]);
|
|
setCharts((res.chart_data || []) as ChartData[]);
|
|
}
|
|
} catch (e: any) {
|
|
if (live) setError(e.message || 'Failed to load analytics');
|
|
} finally {
|
|
if (live) setLoading(false);
|
|
}
|
|
}
|
|
run();
|
|
return () => { live = false; };
|
|
}, []);
|
|
|
|
if (loading) return <div className="h-full flex items-center justify-center"><Spinner label="Loading Analytics..." /></div>;
|
|
if (error) return <EmptyState title="Analytics Error" hint={error} />;
|
|
|
|
|
|
|
|
const renderChart = (chart: ChartData, index: number) => {
|
|
const data = chart.rows.map(r => ({
|
|
name: r.dimension || 'Unknown',
|
|
value: r.value
|
|
}));
|
|
|
|
const title = chart.key.split('_').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
|
|
// Decide chart type based on key
|
|
if (chart.key === 'status_overview') {
|
|
return (
|
|
<div key={chart.chart_uid} className="bg-white rounded-2xl shadow-[0_2px_12px_-4px_rgba(0,0,0,0.08)] border border-slate-100 p-5 flex flex-col gap-4">
|
|
<div>
|
|
<h2 className="text-[15px] font-bold text-slate-800 tracking-tight">{title}</h2>
|
|
</div>
|
|
<div className="h-64 w-full">
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<PieChart>
|
|
<Pie
|
|
data={data}
|
|
cx="50%"
|
|
cy="50%"
|
|
innerRadius={60}
|
|
outerRadius={80}
|
|
paddingAngle={5}
|
|
dataKey="value"
|
|
>
|
|
{data.map((_, index) => (
|
|
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
|
))}
|
|
</Pie>
|
|
<RechartsTooltip
|
|
contentStyle={{ borderRadius: '8px', border: 'none', boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)' }}
|
|
/>
|
|
<Legend />
|
|
</PieChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div key={chart.chart_uid} className="bg-white rounded-2xl shadow-[0_2px_12px_-4px_rgba(0,0,0,0.08)] border border-slate-100 p-5 flex flex-col gap-4">
|
|
<div>
|
|
<h2 className="text-[15px] font-bold text-slate-800 tracking-tight">{title}</h2>
|
|
</div>
|
|
<div className="h-64 w-full">
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<BarChart data={data} margin={{ top: 10, right: 10, left: -20, bottom: 0 }}>
|
|
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#f1f5f9" />
|
|
<XAxis dataKey="name" tick={{ fontSize: 12, fill: '#64748b' }} axisLine={false} tickLine={false} />
|
|
<YAxis tick={{ fontSize: 12, fill: '#64748b' }} axisLine={false} tickLine={false} />
|
|
<RechartsTooltip
|
|
cursor={{ fill: '#f8fafc' }}
|
|
contentStyle={{ borderRadius: '8px', border: 'none', boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)' }}
|
|
/>
|
|
<Bar dataKey="value" fill={COLORS[index % COLORS.length]} radius={[4, 4, 0, 0]} barSize={40} />
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div className="flex flex-col gap-6 animate-in fade-in slide-in-from-bottom-2 duration-300">
|
|
<div className="flex items-center gap-2 px-1">
|
|
<TrendingUp className="text-sunrise-500" size={24} />
|
|
<h1 className="text-xl font-extrabold text-slate-800 tracking-tight">Analytics Overview</h1>
|
|
</div>
|
|
|
|
{/* Tiles Grid */}
|
|
<StatsTiles tiles={tiles as any} />
|
|
|
|
{/* Charts Grid */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 pb-4">
|
|
{charts.map((chart, index) => renderChart(chart, index))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|