Merged two dashboards into a single unified dashboard
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||||
import Layout from './components/Layout.tsx';
|
import Layout from './components/Layout.tsx';
|
||||||
import Dashboard from './pages/Dashboard.tsx';
|
import Dashboard from './pages/Dashboard.tsx';
|
||||||
import StoreDashboard from './pages/StoreDashboard.tsx';
|
|
||||||
import BaseMenu from './pages/BaseMenu.tsx';
|
import BaseMenu from './pages/BaseMenu.tsx';
|
||||||
import Products from './pages/Products.tsx';
|
import Products from './pages/Products.tsx';
|
||||||
import Orders from './pages/Orders.tsx';
|
import Orders from './pages/Orders.tsx';
|
||||||
@@ -48,7 +47,6 @@ function App() {
|
|||||||
}>
|
}>
|
||||||
<Route index element={<Navigate to="/dashboard" replace />} />
|
<Route index element={<Navigate to="/dashboard" replace />} />
|
||||||
<Route path="dashboard" element={<Dashboard />} />
|
<Route path="dashboard" element={<Dashboard />} />
|
||||||
<Route path="store-dashboard" element={<StoreDashboard />} />
|
|
||||||
|
|
||||||
{/* Sale */}
|
{/* Sale */}
|
||||||
<Route path="sale/orders" element={<Orders />} />
|
<Route path="sale/orders" element={<Orders />} />
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ interface MenuItem {
|
|||||||
|
|
||||||
const menuItems: MenuItem[] = [
|
const menuItems: MenuItem[] = [
|
||||||
{ title: 'Dashboard', icon: Gauge, path: '/dashboard' },
|
{ title: 'Dashboard', icon: Gauge, path: '/dashboard' },
|
||||||
{ title: 'Store Dashboard', icon: LayoutGrid, path: '/store-dashboard' },
|
|
||||||
{
|
{
|
||||||
title: 'Sale',
|
title: 'Sale',
|
||||||
icon: Receipt,
|
icon: Receipt,
|
||||||
@@ -97,7 +96,7 @@ const menuItems: MenuItem[] = [
|
|||||||
icon: Store,
|
icon: Store,
|
||||||
subMenu: [
|
subMenu: [
|
||||||
{ title: 'Terminals', path: '/stores/terminals' },
|
{ title: 'Terminals', path: '/stores/terminals' },
|
||||||
{ title: 'Managers', path: '/stores/managers' },
|
{ title: 'Managers', path: '/stores/staffs' },
|
||||||
{ title: 'Staffs', path: '/stores/staffs' },
|
{ title: 'Staffs', path: '/stores/staffs' },
|
||||||
{ title: 'Stalls', path: '/stores/stalls' }
|
{ title: 'Stalls', path: '/stores/stalls' }
|
||||||
]
|
]
|
||||||
@@ -131,7 +130,6 @@ const Sidebar = () => {
|
|||||||
// Map title to permission ID
|
// Map title to permission ID
|
||||||
const permissionMap: Record<string, string> = {
|
const permissionMap: Record<string, string> = {
|
||||||
'Dashboard': 'dashboard',
|
'Dashboard': 'dashboard',
|
||||||
'Store Dashboard': 'dashboard',
|
|
||||||
'Sale': 'sale',
|
'Sale': 'sale',
|
||||||
'Customers': 'customers',
|
'Customers': 'customers',
|
||||||
'Purchases': 'purchases',
|
'Purchases': 'purchases',
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import {
|
|||||||
BarChart3,
|
BarChart3,
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
Flame,
|
Flame,
|
||||||
TrendingUp
|
TrendingUp,
|
||||||
|
UserMinus
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
AreaChart,
|
AreaChart,
|
||||||
@@ -29,6 +30,7 @@ import { motion } from 'framer-motion';
|
|||||||
const Dashboard = () => {
|
const Dashboard = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [activeTab, setActiveTab] = useState('Sales');
|
const [activeTab, setActiveTab] = useState('Sales');
|
||||||
|
const [dashboardMode, setDashboardMode] = useState<'business' | 'store'>('business');
|
||||||
const [timeRange, setTimeRange] = useState('Today');
|
const [timeRange, setTimeRange] = useState('Today');
|
||||||
const [customDates, setCustomDates] = useState({ from: '', to: '' });
|
const [customDates, setCustomDates] = useState({ from: '', to: '' });
|
||||||
const [data, setData] = useState<any>(null);
|
const [data, setData] = useState<any>(null);
|
||||||
@@ -36,7 +38,6 @@ const Dashboard = () => {
|
|||||||
|
|
||||||
const toLocalISOString = (date: Date) => {
|
const toLocalISOString = (date: Date) => {
|
||||||
const tzo = -date.getTimezoneOffset(),
|
const tzo = -date.getTimezoneOffset(),
|
||||||
dif = tzo >= 0 ? '+' : '-',
|
|
||||||
pad = (num: number) => {
|
pad = (num: number) => {
|
||||||
const norm = Math.floor(Math.abs(num));
|
const norm = Math.floor(Math.abs(num));
|
||||||
return (norm < 10 ? '0' : '') + norm;
|
return (norm < 10 ? '0' : '') + norm;
|
||||||
@@ -136,9 +137,12 @@ const Dashboard = () => {
|
|||||||
|
|
||||||
const { stats, storeOverview, hourlySales, insights, trendingItems } = data;
|
const { stats, storeOverview, hourlySales, insights, trendingItems } = data;
|
||||||
|
|
||||||
const pieData = [
|
const pieData = dashboardMode === 'business' ? [
|
||||||
{ name: 'Full Payment', value: stats.periodRevenue, color: '#8b5cf6' },
|
{ name: 'RITZ Spend', value: stats.periodRevenue, color: '#8b5cf6' },
|
||||||
{ name: 'Credit', value: 0, color: '#fbbf24' }
|
{ name: 'Other', value: 0, color: '#fbbf24' }
|
||||||
|
] : [
|
||||||
|
{ name: 'Store Revenue', value: stats.periodStoreRevenue, color: '#8b5cf6' },
|
||||||
|
{ name: 'Other', value: 0, color: '#fbbf24' }
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -150,7 +154,7 @@ const Dashboard = () => {
|
|||||||
<button title="Navigate back to previous section" className="p-1.5 hover:bg-slate-100 rounded-lg transition-all">
|
<button title="Navigate back to previous section" className="p-1.5 hover:bg-slate-100 rounded-lg transition-all">
|
||||||
<ArrowRight className="rotate-180 text-slate-400" size={18} />
|
<ArrowRight className="rotate-180 text-slate-400" size={18} />
|
||||||
</button>
|
</button>
|
||||||
<h2 className="text-xs font-black text-[#003317] uppercase tracking-widest">Dashboard</h2>
|
<h2 className="text-xs font-black text-[#003317] uppercase tracking-widest">Dashboard Overview</h2>
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-2xl font-black text-slate-800">
|
<h1 className="text-2xl font-black text-slate-800">
|
||||||
Good morning, {(() => {
|
Good morning, {(() => {
|
||||||
@@ -160,7 +164,7 @@ const Dashboard = () => {
|
|||||||
</h1>
|
</h1>
|
||||||
<p className="text-[10px] text-slate-400 font-medium flex items-center gap-1.5">
|
<p className="text-[10px] text-slate-400 font-medium flex items-center gap-1.5">
|
||||||
<span className="p-0.5 bg-slate-200 rounded text-slate-500">i</span>
|
<span className="p-0.5 bg-slate-200 rounded text-slate-500">i</span>
|
||||||
The default time settings for the merchant view are from 12:00 AM to 11:59 PM.
|
Combined metrics from both the Business (Ritz tokens) and Store performance parameters.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -180,10 +184,31 @@ const Dashboard = () => {
|
|||||||
<BarChart3 size={16} />
|
<BarChart3 size={16} />
|
||||||
View Full Report
|
View Full Report
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Mode Toggles */}
|
||||||
|
<div className="flex items-center justify-start border-b border-slate-200 pb-1 gap-8">
|
||||||
|
<button
|
||||||
|
onClick={() => setDashboardMode('business')}
|
||||||
|
className={`pb-3 text-xs font-black uppercase tracking-widest transition-all relative ${dashboardMode === 'business' ? 'text-[#003317]' : 'text-slate-400 hover:text-[#003317]'}`}
|
||||||
|
>
|
||||||
|
Business Overview (🅡)
|
||||||
|
{dashboardMode === 'business' && (
|
||||||
|
<motion.div layoutId="dashboardModeLine" className="absolute bottom-0 left-0 right-0 h-0.5 bg-[#003317]" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setDashboardMode('store')}
|
||||||
|
className={`pb-3 text-xs font-black uppercase tracking-widest transition-all relative ${dashboardMode === 'store' ? 'text-[#003317]' : 'text-slate-400 hover:text-[#003317]'}`}
|
||||||
|
>
|
||||||
|
Store Revenue (₹)
|
||||||
|
{dashboardMode === 'store' && (
|
||||||
|
<motion.div layoutId="dashboardModeLine" className="absolute bottom-0 left-0 right-0 h-0.5 bg-[#003317]" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Main Stats Grid */}
|
{/* Main Stats Grid */}
|
||||||
<div className="grid grid-cols-12 gap-6">
|
<div className="grid grid-cols-12 gap-6">
|
||||||
{/* Sales Chart Section */}
|
{/* Sales Chart Section */}
|
||||||
@@ -207,7 +232,9 @@ const Dashboard = () => {
|
|||||||
<div className="flex items-center justify-end mb-4 gap-4">
|
<div className="flex items-center justify-end mb-4 gap-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-2.5 h-2.5 rounded-full border-2 border-[#003317]/30 bg-[#003317]/10" />
|
<div className="w-2.5 h-2.5 rounded-full border-2 border-[#003317]/30 bg-[#003317]/10" />
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">{timeRange}'s Revenue</span>
|
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">
|
||||||
|
{timeRange}'s {dashboardMode === 'business' ? 'Circulation' : 'Revenue'}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -222,7 +249,12 @@ const Dashboard = () => {
|
|||||||
</defs>
|
</defs>
|
||||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#f1f5f9" />
|
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#f1f5f9" />
|
||||||
<XAxis dataKey="time" axisLine={false} tickLine={false} tick={{ fill: '#94a3b8', fontSize: 10, fontWeight: 700 }} />
|
<XAxis dataKey="time" axisLine={false} tickLine={false} tick={{ fill: '#94a3b8', fontSize: 10, fontWeight: 700 }} />
|
||||||
<YAxis axisLine={false} tickLine={false} tick={{ fill: '#94a3b8', fontSize: 10, fontWeight: 700 }} tickFormatter={(v) => v >= 1000 ? `🅡${v / 1000}k` : `🅡${v}`} />
|
<YAxis
|
||||||
|
axisLine={false}
|
||||||
|
tickLine={false}
|
||||||
|
tick={{ fill: '#94a3b8', fontSize: 10, fontWeight: 700 }}
|
||||||
|
tickFormatter={(v) => dashboardMode === 'business' ? (v >= 1000 ? `🅡${v / 1000}k` : `🅡${v}`) : (v >= 1000 ? `₹${v / 1000}k` : `₹${v}`)}
|
||||||
|
/>
|
||||||
<Tooltip contentStyle={{ borderRadius: '12px', border: 'none', boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)' }} />
|
<Tooltip contentStyle={{ borderRadius: '12px', border: 'none', boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)' }} />
|
||||||
<Area type="monotone" dataKey="value" stroke="#f43f5e" strokeWidth={3} fillOpacity={1} fill="url(#colorSalesMain)" />
|
<Area type="monotone" dataKey="value" stroke="#f43f5e" strokeWidth={3} fillOpacity={1} fill="url(#colorSalesMain)" />
|
||||||
</AreaChart>
|
</AreaChart>
|
||||||
@@ -234,7 +266,9 @@ const Dashboard = () => {
|
|||||||
<div className="col-span-12 lg:col-span-4 bg-white p-6 rounded-[32px] border border-slate-100 shadow-sm relative">
|
<div className="col-span-12 lg:col-span-4 bg-white p-6 rounded-[32px] border border-slate-100 shadow-sm relative">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h3 className="text-sm font-black text-slate-800 tracking-tight uppercase tracking-widest">Total Revenue</h3>
|
<h3 className="text-sm font-black text-slate-800 tracking-tight uppercase tracking-widest">
|
||||||
|
{dashboardMode === 'business' ? 'Total Spent' : 'Total Revenue'}
|
||||||
|
</h3>
|
||||||
<Target size={14} className="text-slate-300" />
|
<Target size={14} className="text-slate-300" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -260,9 +294,13 @@ const Dashboard = () => {
|
|||||||
</PieChart>
|
</PieChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-center">
|
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-center">
|
||||||
<h2 className="text-3xl font-black text-slate-800 tracking-tighter">🅡{(stats.periodRevenue || 0).toLocaleString()}</h2>
|
<h2 className="text-3xl font-black text-slate-800 tracking-tighter">
|
||||||
|
{dashboardMode === 'business' ? `🅡${(stats.periodRevenue || 0).toLocaleString()}` : `₹${(stats.periodStoreRevenue || 0).toLocaleString()}`}
|
||||||
|
</h2>
|
||||||
<p className="text-[10px] font-black text-slate-400 uppercase tracking-widest mt-1">{timeRange === 'Today' ? 'Today' : timeRange}</p>
|
<p className="text-[10px] font-black text-slate-400 uppercase tracking-widest mt-1">{timeRange === 'Today' ? 'Today' : timeRange}</p>
|
||||||
<p className="text-[8px] font-bold text-slate-300 uppercase tracking-widest mt-0.5">Total: 🅡{(stats.totalSales || 0).toLocaleString()}</p>
|
<p className="text-[8px] font-bold text-slate-300 uppercase tracking-widest mt-0.5">
|
||||||
|
Total: {dashboardMode === 'business' ? `🅡${(stats.totalSales || 0).toLocaleString()}` : `₹${(stats.totalStoreRevenue || 0).toLocaleString()}`}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-6 mt-2">
|
<div className="flex gap-6 mt-2">
|
||||||
{pieData.map(item => (
|
{pieData.map(item => (
|
||||||
@@ -311,7 +349,7 @@ const Dashboard = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Total Orders Card */}
|
{/* Total Orders Card */}
|
||||||
<div title="View detailed order volume and throughput" className="bg-white p-6 rounded-[24px] border border-slate-100 shadow-sm relative h-[180px] flex flex-col justify-between group hover:border-[#003317]/30 transition-all cursor-pointer">
|
<div title="View detailed order volume and throughput" className="bg-white p-6 rounded-[24px] border border-slate-100 shadow-sm relative h-[140px] flex flex-col justify-between group hover:border-[#003317]/30 transition-all cursor-pointer">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="p-2 bg-amber-50 text-amber-500 rounded-xl group-hover:scale-110 transition-transform">
|
<div className="p-2 bg-amber-50 text-amber-500 rounded-xl group-hover:scale-110 transition-transform">
|
||||||
<ShoppingBag size={20} />
|
<ShoppingBag size={20} />
|
||||||
@@ -319,8 +357,8 @@ const Dashboard = () => {
|
|||||||
<h4 className="text-[11px] font-black text-slate-400 uppercase tracking-widest leading-none">Total Orders</h4>
|
<h4 className="text-[11px] font-black text-slate-400 uppercase tracking-widest leading-none">Total Orders</h4>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<h2 className="text-5xl font-black text-slate-800 tracking-tighter">{stats.activeOrders}</h2>
|
<h2 className="text-3xl font-black text-slate-800 tracking-tighter">{stats.activeOrders}</h2>
|
||||||
<div className="w-full h-1 bg-green-500 rounded-full mt-4 shadow-sm" />
|
<div className="w-full h-1 bg-green-500 rounded-full mt-2 shadow-sm" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -336,6 +374,19 @@ const Dashboard = () => {
|
|||||||
<h2 className="text-3xl font-black text-slate-800 tracking-tighter">₹{stats.periodExpenses.toLocaleString()}</h2>
|
<h2 className="text-3xl font-black text-slate-800 tracking-tighter">₹{stats.periodExpenses.toLocaleString()}</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Restricted Accounts Card */}
|
||||||
|
<div title="Monitor suspended customer accounts" className="bg-white p-6 rounded-[24px] border border-slate-100 shadow-sm h-[130px] flex flex-col justify-between group cursor-pointer hover:border-red-500/30 transition-all">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="p-2 bg-red-50 text-red-500 rounded-xl group-hover:scale-110 transition-transform">
|
||||||
|
<UserMinus size={20} />
|
||||||
|
</div>
|
||||||
|
<h4 className="text-[11px] font-black text-slate-400 uppercase tracking-widest leading-none">Restricted Accounts</h4>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<h2 className="text-3xl font-black text-slate-800 tracking-tighter">{stats?.suspendedUserCount || 0}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,438 +0,0 @@
|
|||||||
import { apiFetch } from '../api';
|
|
||||||
import { useState, useEffect } from 'react';
|
|
||||||
import {
|
|
||||||
ChevronRight,
|
|
||||||
ShoppingBag,
|
|
||||||
Wallet,
|
|
||||||
ArrowRight,
|
|
||||||
RotateCcw,
|
|
||||||
Star,
|
|
||||||
UserMinus
|
|
||||||
} from 'lucide-react';
|
|
||||||
import {
|
|
||||||
AreaChart,
|
|
||||||
Area,
|
|
||||||
XAxis,
|
|
||||||
YAxis,
|
|
||||||
CartesianGrid,
|
|
||||||
Tooltip,
|
|
||||||
ResponsiveContainer,
|
|
||||||
PieChart,
|
|
||||||
Pie,
|
|
||||||
Cell
|
|
||||||
} from 'recharts';
|
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
|
|
||||||
|
|
||||||
const StoreDashboard = () => {
|
|
||||||
const [activeTab, setActiveTab] = useState('Sales');
|
|
||||||
const [timeRange, setTimeRange] = useState('Today');
|
|
||||||
const [customDates, setCustomDates] = useState({ from: '', to: '' });
|
|
||||||
const [stats, setStats] = useState({
|
|
||||||
totalSales: 0,
|
|
||||||
activeOrders: 0,
|
|
||||||
dailyCustomers: 0,
|
|
||||||
revenueGrowth: 0,
|
|
||||||
suspendedUserCount: 0
|
|
||||||
});
|
|
||||||
const [trendingItems, setTrendingItems] = useState<any[]>([]);
|
|
||||||
const [salesData, setSalesData] = useState<any[]>([]);
|
|
||||||
const [insights, setInsights] = useState<any[]>([]);
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
|
|
||||||
const formatCurrency = (val: any) => {
|
|
||||||
const num = Number(val);
|
|
||||||
return isNaN(num) ? '0' : num.toLocaleString();
|
|
||||||
};
|
|
||||||
|
|
||||||
const toLocalISOString = (date: Date) => {
|
|
||||||
const tzo = -date.getTimezoneOffset(),
|
|
||||||
pad = (num: number) => {
|
|
||||||
const norm = Math.floor(Math.abs(num));
|
|
||||||
return (norm < 10 ? '0' : '') + norm;
|
|
||||||
};
|
|
||||||
return date.getFullYear() +
|
|
||||||
'-' + pad(date.getMonth() + 1) +
|
|
||||||
'-' + pad(date.getDate()) +
|
|
||||||
'T' + pad(date.getHours()) +
|
|
||||||
':' + pad(date.getMinutes()) +
|
|
||||||
':' + pad(date.getSeconds()) +
|
|
||||||
'.' + pad(date.getMilliseconds());
|
|
||||||
};
|
|
||||||
|
|
||||||
const getRangeDates = (range: string) => {
|
|
||||||
const now = new Date();
|
|
||||||
const start = new Date();
|
|
||||||
const end = new Date();
|
|
||||||
|
|
||||||
// Set end to end of today
|
|
||||||
end.setHours(23, 59, 59, 999);
|
|
||||||
|
|
||||||
switch (range) {
|
|
||||||
case 'Today':
|
|
||||||
start.setHours(0, 0, 0, 0);
|
|
||||||
break;
|
|
||||||
case 'Yesterday':
|
|
||||||
start.setDate(now.getDate() - 1);
|
|
||||||
start.setHours(0, 0, 0, 0);
|
|
||||||
end.setDate(now.getDate() - 1);
|
|
||||||
end.setHours(23, 59, 59, 999);
|
|
||||||
break;
|
|
||||||
case 'Week':
|
|
||||||
start.setDate(now.getDate() - 7);
|
|
||||||
start.setHours(0, 0, 0, 0);
|
|
||||||
break;
|
|
||||||
case '30 Days':
|
|
||||||
start.setDate(now.getDate() - 30);
|
|
||||||
start.setHours(0, 0, 0, 0);
|
|
||||||
break;
|
|
||||||
case 'Custom':
|
|
||||||
if (customDates.from && customDates.to) {
|
|
||||||
const from = new Date(customDates.from);
|
|
||||||
from.setHours(0, 0, 0, 0);
|
|
||||||
const to = new Date(customDates.to);
|
|
||||||
to.setHours(23, 59, 59, 999);
|
|
||||||
return { from: toLocalISOString(from), to: toLocalISOString(to) };
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
default:
|
|
||||||
start.setHours(0, 0, 0, 0);
|
|
||||||
}
|
|
||||||
return { from: toLocalISOString(start), to: toLocalISOString(end) };
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchStats = async () => {
|
|
||||||
try {
|
|
||||||
setIsLoading(true);
|
|
||||||
const range = getRangeDates(timeRange);
|
|
||||||
if (!range) {
|
|
||||||
setIsLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
params.append('from', range.from);
|
|
||||||
params.append('to', range.to);
|
|
||||||
|
|
||||||
const response = await apiFetch(`/api/dashboard/stats?${params.toString()}`);
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
console.log('Dashboard data received successfully:', data);
|
|
||||||
|
|
||||||
if (data.stats) {
|
|
||||||
setStats({
|
|
||||||
totalSales: data.stats.periodStoreRevenue,
|
|
||||||
activeOrders: data.stats.activeOrders,
|
|
||||||
dailyCustomers: data.stats.dailyCustomers,
|
|
||||||
revenueGrowth: data.stats.growth,
|
|
||||||
suspendedUserCount: data.stats.suspendedUserCount
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.storeOverview && data.storeOverview.length > 0) {
|
|
||||||
const ritStore = data.storeOverview.find((s: any) => s.name === 'Tillo Canteen');
|
|
||||||
if (ritStore) {
|
|
||||||
setStats(prev => ({
|
|
||||||
...prev,
|
|
||||||
totalSales: data.stats.periodStoreRevenue,
|
|
||||||
activeOrders: ritStore.orders,
|
|
||||||
dailyCustomers: ritStore.orders * 0.9,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.trendingItems) setTrendingItems(data.trendingItems);
|
|
||||||
if (data.hourlySales) setSalesData(data.hourlySales);
|
|
||||||
if (data.insights) setInsights(data.insights);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching dashboard stats:', error);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
fetchStats();
|
|
||||||
}, [timeRange, customDates]);
|
|
||||||
|
|
||||||
const pieData = [
|
|
||||||
{ name: 'Full Payment', value: Number(stats.totalSales) || 0, color: '#8b5cf6' },
|
|
||||||
{ name: 'Credit', value: 0, color: '#fbbf24' }
|
|
||||||
];
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<div className="h-screen flex items-center justify-center bg-slate-50/50">
|
|
||||||
<motion.div
|
|
||||||
animate={{ scale: [1, 1.1, 1], opacity: [0.5, 1, 0.5] }}
|
|
||||||
transition={{ repeat: Infinity, duration: 1.5 }}
|
|
||||||
className="text-[#003317] font-black uppercase tracking-widest text-sm"
|
|
||||||
>
|
|
||||||
Analyzing Store Data...
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="p-8 space-y-8 bg-slate-50/50 min-h-screen font-inter">
|
|
||||||
{/* Top Header */}
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<div className="flex items-center gap-2 mb-1">
|
|
||||||
<button title="Return to previous screen" className="p-1.5 hover:bg-slate-100 rounded-lg transition-all">
|
|
||||||
<ArrowRight className="rotate-180 text-slate-400" size={18} />
|
|
||||||
</button>
|
|
||||||
<h2 className="text-xs font-black text-[#003317] uppercase tracking-widest">Store Dashboard</h2>
|
|
||||||
</div>
|
|
||||||
<h1 className="text-2xl font-black text-slate-800">
|
|
||||||
Good morning, {(() => {
|
|
||||||
const saved = localStorage.getItem('systemUser');
|
|
||||||
return saved ? JSON.parse(saved).name : 'Partner';
|
|
||||||
})()}
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
{/* Action buttons or profile placeholder can go here if needed later */}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Main Stats Grid */}
|
|
||||||
<div className="grid grid-cols-12 gap-6">
|
|
||||||
{/* Sales Chart Section */}
|
|
||||||
<div className="col-span-12 lg:col-span-5 bg-white p-6 rounded-[32px] border border-slate-100 shadow-sm relative overflow-hidden group">
|
|
||||||
<div className="flex items-center gap-8 mb-8 border-b border-slate-50">
|
|
||||||
{['Sales', 'Payments'].map(tab => (
|
|
||||||
<button
|
|
||||||
key={tab}
|
|
||||||
title={`Visualize ${tab.toLowerCase()} throughput data`}
|
|
||||||
onClick={() => setActiveTab(tab)}
|
|
||||||
className={`pb-4 text-[11px] font-black uppercase tracking-widest transition-all relative ${activeTab === tab ? 'text-[#003317]' : 'text-slate-400 hover:text-slate-600'}`}
|
|
||||||
>
|
|
||||||
{tab}
|
|
||||||
{activeTab === tab && (
|
|
||||||
<motion.div layoutId="tabLineStore" className="absolute bottom-0 left-0 right-0 h-0.5 bg-[#003317]" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="h-[280px] w-full mt-4">
|
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
|
||||||
<AreaChart data={salesData}>
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="colorSalesStore" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop offset="5%" stopColor="#f43f5e" stopOpacity={0.2} />
|
|
||||||
<stop offset="95%" stopColor="#f43f5e" stopOpacity={0} />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#f1f5f9" />
|
|
||||||
<XAxis dataKey="time" axisLine={false} tickLine={false} tick={{ fill: '#94a3b8', fontSize: 10, fontWeight: 700 }} />
|
|
||||||
<YAxis axisLine={false} tickLine={false} tick={{ fill: '#94a3b8', fontSize: 10, fontWeight: 700 }} tickFormatter={(v) => v >= 1000 ? `R${v / 1000}k` : `R${v}`} />
|
|
||||||
<Tooltip contentStyle={{ borderRadius: '12px', border: 'none', boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)' }} />
|
|
||||||
<Area type="monotone" dataKey="value" stroke="#f43f5e" strokeWidth={3} fillOpacity={1} fill="url(#colorSalesStore)" />
|
|
||||||
</AreaChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Total Sales Gauge */}
|
|
||||||
<div className="col-span-12 lg:col-span-4 bg-white p-6 rounded-[32px] border border-slate-100 shadow-sm relative">
|
|
||||||
<div className="relative h-[280px] flex flex-col items-center justify-center mt-8">
|
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
|
||||||
<PieChart>
|
|
||||||
<Pie
|
|
||||||
data={pieData}
|
|
||||||
cx="50%"
|
|
||||||
cy="50%"
|
|
||||||
innerRadius={80}
|
|
||||||
outerRadius={105}
|
|
||||||
paddingAngle={0}
|
|
||||||
dataKey="value"
|
|
||||||
startAngle={210}
|
|
||||||
endAngle={-150}
|
|
||||||
>
|
|
||||||
{pieData.map((entry, index) => (
|
|
||||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
|
||||||
))}
|
|
||||||
</Pie>
|
|
||||||
</PieChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-center">
|
|
||||||
<h2 className="text-3xl font-black text-slate-800 tracking-tighter">₹{formatCurrency(stats.totalSales)}</h2>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-6 mt-4">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="w-2.5 h-2.5 rounded-full bg-[#8b5cf6]" />
|
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Full-Payment</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="w-2.5 h-2.5 rounded-full bg-[#fbbf24]" />
|
|
||||||
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Credit</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right Stats Column */}
|
|
||||||
<div className="col-span-12 lg:col-span-3 space-y-6">
|
|
||||||
{/* Filters */}
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
<div className="flex items-center justify-between overflow-x-auto gap-2 pb-2 scrollbar-none">
|
|
||||||
{['Yesterday', 'Today', 'Week', '30 Days', 'Custom'].map(range => (
|
|
||||||
<button
|
|
||||||
key={range}
|
|
||||||
title={`Filter metrics by ${range.toLowerCase()}`}
|
|
||||||
onClick={() => setTimeRange(range)}
|
|
||||||
className={`whitespace-nowrap px-2 py-2 text-[10px] font-black uppercase tracking-tighter transition-all relative ${timeRange === range ? 'text-[#003317]' : 'text-slate-400 hover:text-slate-600'}`}
|
|
||||||
>
|
|
||||||
{range}
|
|
||||||
{timeRange === range && (
|
|
||||||
<motion.div layoutId="rangeLineStore" className="absolute bottom-0 left-0 right-0 h-0.5 bg-[#003317]" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{timeRange === 'Custom' && (
|
|
||||||
<div className="flex items-center gap-2 animate-in fade-in slide-in-from-top-1 duration-300">
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
value={customDates.from}
|
|
||||||
onChange={(e) => setCustomDates({ ...customDates, from: e.target.value })}
|
|
||||||
className="flex-1 bg-white border border-slate-200 rounded-lg px-2 py-1.5 text-[10px] font-bold text-slate-600 outline-none focus:border-[#003317]"
|
|
||||||
/>
|
|
||||||
<span className="text-slate-300 text-[10px] font-bold">to</span>
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
value={customDates.to}
|
|
||||||
onChange={(e) => setCustomDates({ ...customDates, to: e.target.value })}
|
|
||||||
className="flex-1 bg-white border border-slate-200 rounded-lg px-2 py-1.5 text-[10px] font-bold text-slate-600 outline-none focus:border-[#003317]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Total Orders Card */}
|
|
||||||
<div title="View detailed store volume and throughput" className="bg-white p-6 rounded-[24px] border border-slate-100 shadow-sm relative h-[180px] flex flex-col justify-between group cursor-pointer hover:border-[#003317]/30 transition-all">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="p-2 bg-amber-50 text-amber-500 rounded-xl group-hover:scale-110 transition-transform">
|
|
||||||
<ShoppingBag size={20} />
|
|
||||||
</div>
|
|
||||||
<h4 className="text-[11px] font-black text-slate-400 uppercase tracking-widest leading-none">Total Sales</h4>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col items-center">
|
|
||||||
<h2 className="text-5xl font-black text-slate-800 tracking-tighter">{stats.activeOrders}</h2>
|
|
||||||
<div className="w-full h-1 bg-green-500 rounded-full mt-4 shadow-sm" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Restricted Accounts Card */}
|
|
||||||
<div title="Monitor suspended customer accounts" className="bg-white p-6 rounded-[24px] border border-slate-100 shadow-sm h-[130px] flex flex-col justify-between group cursor-pointer hover:border-red-500/30 transition-all">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="p-2 bg-red-50 text-red-500 rounded-xl group-hover:scale-110 transition-transform">
|
|
||||||
<UserMinus size={20} />
|
|
||||||
</div>
|
|
||||||
<h4 className="text-[11px] font-black text-slate-400 uppercase tracking-widest leading-none">Restricted Accounts</h4>
|
|
||||||
</div>
|
|
||||||
<div className="text-center">
|
|
||||||
<h2 className="text-4xl font-black text-slate-800 tracking-tighter">{stats?.suspendedUserCount || 0}</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Expenses Card */}
|
|
||||||
<div title="Monitor store operational expenditures" className="bg-white p-6 rounded-[24px] border border-slate-100 shadow-sm h-[130px] flex flex-col justify-between group cursor-pointer hover:border-[#003317]/30 transition-all">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="p-2 bg-amber-100 text-[#003317] rounded-xl shadow-sm">
|
|
||||||
<Wallet size={20} />
|
|
||||||
</div>
|
|
||||||
<h4 className="text-[11px] font-black text-slate-400 uppercase tracking-widest leading-none">Expenses</h4>
|
|
||||||
</div>
|
|
||||||
<div className="text-center">
|
|
||||||
<h2 className="text-4xl font-black text-slate-800 tracking-tighter">₹0</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Bottom Section */}
|
|
||||||
<div className="grid grid-cols-12 gap-8 mt-4 pb-12">
|
|
||||||
{/* Trending Items */}
|
|
||||||
<div className="col-span-12 lg:col-span-6 bg-white rounded-[32px] border border-slate-100 shadow-sm p-6">
|
|
||||||
<div className="flex items-center justify-between mb-8">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<h3 className="text-sm font-black text-slate-800 tracking-tight uppercase tracking-widest">Trending Items</h3>
|
|
||||||
<div className="p-1 px-2 bg-indigo-50 rounded-lg text-indigo-400">
|
|
||||||
<Star size={12} fill="currentColor" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1.5 p-1 bg-slate-50 rounded-lg border border-slate-100">
|
|
||||||
<button title="View inventory metrics" className="p-1.5 text-slate-400 hover:text-slate-600 transition-all"><ShoppingBag size={14} /></button>
|
|
||||||
<button title="View price distributions" className="text-[10px] font-black text-slate-400 px-1">₹</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="overflow-x-auto">
|
|
||||||
<table className="w-full">
|
|
||||||
<thead>
|
|
||||||
<tr className="text-left border-b border-slate-50">
|
|
||||||
<th className="pb-4 text-[10px] font-black text-slate-400 uppercase tracking-widest">Items</th>
|
|
||||||
<th className="pb-4 text-[10px] font-black text-slate-400 uppercase tracking-widest text-right">Sold Quantity</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="divide-y divide-slate-50">
|
|
||||||
{trendingItems.map((item, idx) => (
|
|
||||||
<tr key={idx} className="group cursor-pointer hover:bg-slate-50/50 transition-all">
|
|
||||||
<td className="py-4">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="w-12 h-12 rounded-xl overflow-hidden bg-slate-100 border border-slate-50 group-hover:scale-105 transition-transform flex items-center justify-center">
|
|
||||||
{item.imageUrl ? (
|
|
||||||
<img src={item.imageUrl} alt={item.name} className="w-full h-full object-cover" />
|
|
||||||
) : (
|
|
||||||
<div className="w-full h-full bg-slate-100" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<p className="text-[9px] font-black text-[#003317] uppercase tracking-widest brightness-110">{item.category}</p>
|
|
||||||
<p className="text-xs font-black text-slate-800 uppercase tracking-tight">{item.name}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="py-4 text-right">
|
|
||||||
<span className="text-sm font-black text-slate-800">{item.orderCount}</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Store Insights */}
|
|
||||||
<div className="col-span-12 lg:col-span-6 bg-white rounded-[32px] border border-slate-100 shadow-sm p-6 flex flex-col">
|
|
||||||
<h3 className="text-sm font-black text-slate-800 tracking-tight mb-8 uppercase tracking-widest">Store Insights</h3>
|
|
||||||
|
|
||||||
<div className="space-y-4 flex-1 overflow-y-auto max-h-[480px] custom-scrollbar pr-2">
|
|
||||||
{insights.map((insight, idx) => (
|
|
||||||
<div key={idx} className={`p-4 rounded-2xl border transition-all hover:translate-x-1 ${insight.color} flex items-center gap-3`}>
|
|
||||||
<div className="w-6 h-6 rounded-full bg-white/50 backdrop-blur-sm flex items-center justify-center shrink-0">
|
|
||||||
<Star size={12} className="opacity-70" />
|
|
||||||
</div>
|
|
||||||
<p className="text-[11px] font-bold leading-relaxed">{insight.text}</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-8 flex justify-end">
|
|
||||||
<button title="Analyze subsequent business cycles" className="flex items-center gap-2 px-6 py-2.5 bg-[#003317]/5 hover:bg-[#003317]/10 text-[#003317] text-[11px] font-black uppercase tracking-widest rounded-2xl transition-all group">
|
|
||||||
Next
|
|
||||||
<ArrowRight size={14} className="group-hover:translate-x-1 transition-transform" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default StoreDashboard;
|
|
||||||
Reference in New Issue
Block a user