analytics page added

This commit is contained in:
suryacp23 2026-07-15 10:32:11 +05:30
parent 408e0c2888
commit cbbfe76eab
5 changed files with 192 additions and 7 deletions

View File

@ -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>

View File

@ -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',

View 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>
);
}

View File

@ -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

View File

@ -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' },