analytics page added
This commit is contained in:
parent
408e0c2888
commit
cbbfe76eab
@ -6,7 +6,7 @@ import { OrdersPage } from './screens/OrdersPage'
|
||||
import { CallsPage } from './screens/CallsPage'
|
||||
import { StoresPage } from './screens/StoresPage'
|
||||
import { DailyLogsPage } from './screens/DailyLogsPage'
|
||||
|
||||
import { AnalyticsPage } from './screens/AnalyticsPage'
|
||||
function App() {
|
||||
return (
|
||||
<AuthProvider>
|
||||
@ -25,6 +25,8 @@ function App() {
|
||||
|
||||
<Route path="/daily" element={<DailyLogsPage />} />
|
||||
<Route path="/daily/:instanceId" element={<DailyLogsPage />} />
|
||||
|
||||
<Route path="/analytics" element={<AnalyticsPage />} />
|
||||
</Route>
|
||||
<Route path="*" element={<Navigate to="/orders" replace />} />
|
||||
</Routes>
|
||||
|
||||
@ -56,6 +56,8 @@ export const ORDER_BOOKING = {
|
||||
recordViews: {
|
||||
ORDERS: '1e424561-9a27-44f5-9b68-64d63296d837',
|
||||
CALLS: '56b21b46-6d2c-4e10-b5a3-c63d058d0afa',
|
||||
ANALYTICS_ORDERS: '63714ac2-c9fb-40e2-abba-d4081a70b768',
|
||||
ANALYTICS: '56b21b46-6d2c-4e10-b5a3-c63d058d0afa'
|
||||
},
|
||||
detailViews: {
|
||||
ORDERS: '0804c6c3-6cf9-4050-94bb-fa48dd5de87d',
|
||||
|
||||
177
src/screens/AnalyticsPage.tsx
Normal file
177
src/screens/AnalyticsPage.tsx
Normal file
@ -0,0 +1,177 @@
|
||||
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 { Activity, Package, ShoppingBag, BarChart3, TrendingUp, Phone } from 'lucide-react';
|
||||
import { formatValue } from '../lib/format';
|
||||
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} />;
|
||||
|
||||
// Tile Helper
|
||||
const getTileLabelAndIcon = (key: string) => {
|
||||
switch (key) {
|
||||
case 'total_orders': return { label: 'Total Orders', icon: ShoppingBag, color: 'text-indigo-500', bg: 'bg-indigo-50' };
|
||||
case 'total_bags': return { label: 'Total Bags', icon: Package, color: 'text-amber-500', bg: 'bg-amber-50' };
|
||||
case 'total_kgs': return { label: 'Total KGs', icon: Activity, color: 'text-emerald-500', bg: 'bg-emerald-50' };
|
||||
case 'total_calls': return { label: 'Total Calls', icon: Phone, color: 'text-blue-500', bg: 'bg-blue-50' };
|
||||
case 'total_productive_calls': return { label: 'Productive Calls', icon: TrendingUp, color: 'text-green-500', bg: 'bg-green-50' };
|
||||
case 'no_order_calls': return { label: 'No Order Calls', icon: Activity, color: 'text-red-500', bg: 'bg-red-50' };
|
||||
default: return { label: key.replace(/_/g, ' '), icon: BarChart3, color: 'text-slate-500', bg: 'bg-slate-50' };
|
||||
}
|
||||
};
|
||||
|
||||
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((entry, 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 */}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4">
|
||||
{tiles.map((t) => {
|
||||
const { label, icon: Icon, color, bg } = getTileLabelAndIcon(t.key);
|
||||
return (
|
||||
<div key={t.tile_uid} className="bg-white rounded-2xl p-4 shadow-[0_2px_12px_-4px_rgba(0,0,0,0.08)] border border-slate-100 flex flex-col gap-3 transition-transform hover:-translate-y-1 hover:shadow-lg">
|
||||
<div className={`w-10 h-10 rounded-xl flex items-center justify-center ${bg} ${color}`}>
|
||||
<Icon size={20} strokeWidth={2.5} />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-wider line-clamp-1">{label}</span>
|
||||
<span className="text-xl font-black text-slate-800 tracking-tight">
|
||||
{t.key === 'total_kgs' ? Number(t.value).toLocaleString(undefined, { maximumFractionDigits: 1 }) : formatValue(t.value)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
);
|
||||
}
|
||||
@ -52,9 +52,9 @@ export function ConsoleLayout() {
|
||||
|
||||
<nav
|
||||
className="fixed bottom-0 inset-x-0 z-20 h-16 pb-[env(safe-area-inset-bottom)] bg-card border-t border-border-subtle grid shadow-[0_-2px_16px_rgba(11,27,59,0.06)]"
|
||||
style={{ gridTemplateColumns: `repeat(${SCREENS.length}, minmax(0, 1fr))` }}
|
||||
style={{ gridTemplateColumns: `repeat(${SCREENS.filter(t => t.key !== 'analytics').length}, minmax(0, 1fr))` }}
|
||||
>
|
||||
{SCREENS.map((t) => {
|
||||
{SCREENS.filter(t => t.key !== 'analytics').map((t) => {
|
||||
const Icon = t.icon;
|
||||
return (
|
||||
<NavLink
|
||||
|
||||
@ -14,19 +14,23 @@ import {
|
||||
type WiredDetailViewProps,
|
||||
} from '../components/dv';
|
||||
|
||||
export type ScreenKey = 'orders' | 'calls' | 'stores' | 'daily';
|
||||
import { AnalyticsPage } from './AnalyticsPage';
|
||||
import { BarChart2 } from 'lucide-react';
|
||||
|
||||
export type ScreenKey = 'orders' | 'calls' | 'stores' | 'daily' | 'analytics';
|
||||
|
||||
export interface ScreenDef {
|
||||
key: ScreenKey;
|
||||
label: string;
|
||||
icon: LucideIcon;
|
||||
View: (p: WiredRecordViewProps) => React.JSX.Element;
|
||||
Detail: (p: WiredDetailViewProps) => React.JSX.Element;
|
||||
View: (p: WiredRecordViewProps) => React.JSX.Element | React.FC;
|
||||
Detail?: (p: WiredDetailViewProps) => React.JSX.Element;
|
||||
/** Singular noun for the detail title. */
|
||||
noun: string;
|
||||
noun?: string;
|
||||
}
|
||||
|
||||
export const SCREENS: ScreenDef[] = [
|
||||
{ key: 'analytics', label: 'Analytics', icon: BarChart2, View: AnalyticsPage as any },
|
||||
{ key: 'orders', label: 'Orders', icon: ShoppingCart, View: OrdersView, Detail: OrderDetail, noun: 'Order' },
|
||||
{ key: 'calls', label: 'Calls', icon: Phone, View: CallsView, Detail: CallDetail, noun: 'Call' },
|
||||
{ key: 'stores', label: 'Stores', icon: Store, View: StoresView, Detail: StoreDetail, noun: 'Store' },
|
||||
|
||||
Loading…
Reference in New Issue
Block a user