Suspension realtime added

This commit is contained in:
Sidharth Prabhu
2026-06-23 08:55:09 +05:30
parent 39249ab34f
commit b49265eaaf
12 changed files with 2035 additions and 2019 deletions

View File

@@ -115,45 +115,45 @@ const Header = () => {
};
const handleNotificationClick = async (notif: any) => {
try {
await apiFetch(`http://${window.location.hostname}:8080/api/notifications/mark-read/${notif.id}`, { method: 'POST' });
if (notif.link) navigate(notif.link);
setShowNotifications(false);
fetchNotifications();
} catch (error) {
console.error('Error marking notification as read:', error);
}
try {
await apiFetch(`http://${window.location.hostname}:8080/api/notifications/mark-read/${notif.id}`, { method: 'POST' });
if (notif.link) navigate(notif.link);
setShowNotifications(false);
fetchNotifications();
} catch (error) {
console.error('Error marking notification as read:', error);
}
};
const markAllAsRead = async () => {
try {
await apiFetch(`http://${window.location.hostname}:8080/api/notifications/mark-all-read`, { method: 'POST' });
fetchNotifications();
setShowNotifications(false);
} catch (error) {
console.error('Error marking all as read:', error);
}
try {
await apiFetch(`http://${window.location.hostname}:8080/api/notifications/mark-all-read`, { method: 'POST' });
fetchNotifications();
setShowNotifications(false);
} catch (error) {
console.error('Error marking all as read:', error);
}
};
const getNotificationIcon = (type: string) => {
switch(type) {
case 'FEEDBACK': return <MessageSquare size={16} className="text-emerald-500" />;
case 'PURCHASE': return <ShoppingBag size={16} className="text-indigo-500" />;
case 'PRODUCT': return <Package size={16} className="text-amber-500" />;
case 'COUPON': return <Ticket size={16} className="text-rose-500" />;
default: return <Bell size={16} className="text-slate-400" />;
}
switch (type) {
case 'FEEDBACK': return <MessageSquare size={16} className="text-emerald-500" />;
case 'PURCHASE': return <ShoppingBag size={16} className="text-indigo-500" />;
case 'PRODUCT': return <Package size={16} className="text-amber-500" />;
case 'COUPON': return <Ticket size={16} className="text-rose-500" />;
default: return <Bell size={16} className="text-slate-400" />;
}
};
return (
<header className="h-16 bg-white/80 backdrop-blur-md border-b border-[#e2e8f0] px-8 flex items-center justify-between sticky top-0 z-50">
<div className="flex items-center gap-4 flex-1">
<div className="relative group w-full max-w-md" ref={searchRef}>
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-[#64748b] group-focus-within:text-[#0f4475] transition-colors" size={18} />
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-[#64748b] group-focus-within:text-[#003317] transition-colors" size={18} />
<input
type="text"
placeholder="Search pages, settings, or analytics..."
className="w-full bg-gray-50 border border-transparent rounded-lg py-2 pl-10 pr-4 text-sm focus:outline-none focus:bg-white focus:border-[#0f4475]/30 transition-all shadow-sm"
className="w-full bg-gray-50 border border-transparent rounded-lg py-2 pl-10 pr-4 text-sm focus:outline-none focus:bg-white focus:border-[#003317]/30 transition-all shadow-sm"
value={searchTerm}
onChange={(e) => {
setSearchTerm(e.target.value);
@@ -180,14 +180,14 @@ const Header = () => {
key={page.path}
onClick={() => handleNavigate(page.path)}
onMouseEnter={() => setSelectedIndex(index)}
className={`w-full px-4 py-2.5 flex items-center justify-between transition-colors ${index === selectedIndex ? 'bg-[#0f4475]/5' : ''}`}
className={`w-full px-4 py-2.5 flex items-center justify-between transition-colors ${index === selectedIndex ? 'bg-[#003317]/5' : ''}`}
>
<div className="flex items-center gap-3">
<div className={`p-1.5 rounded-lg transition-colors ${index === selectedIndex ? 'bg-[#0f4475] text-white' : 'bg-gray-100 text-gray-400'}`}>
<div className={`p-1.5 rounded-lg transition-colors ${index === selectedIndex ? 'bg-[#003317] text-white' : 'bg-gray-100 text-gray-400'}`}>
{page.icon}
</div>
<div className="text-left">
<div className={`text-sm font-bold ${index === selectedIndex ? 'text-[#0f4475]' : 'text-slate-700'}`}>
<div className={`text-sm font-bold ${index === selectedIndex ? 'text-[#003317]' : 'text-slate-700'}`}>
{page.label}
</div>
<div className="text-[10px] uppercase tracking-widest font-black opacity-40">
@@ -220,72 +220,72 @@ const Header = () => {
<div className="flex items-center gap-4 border-l border-[#e2e8f0] pl-6">
<div className="relative" ref={notificationRef}>
<button
onClick={() => setShowNotifications(!showNotifications)}
className={`relative p-2 rounded-lg transition-all group ${showNotifications ? 'bg-indigo-50 text-[#0f4475]' : 'text-[#64748b] hover:text-[#1e293b] hover:bg-gray-100'}`}
onClick={() => setShowNotifications(!showNotifications)}
className={`relative p-2 rounded-lg transition-all group ${showNotifications ? 'bg-indigo-50 text-[#003317]' : 'text-[#64748b] hover:text-[#1e293b] hover:bg-gray-100'}`}
>
<Bell size={20} />
{notifications.length > 0 && (
<span className="absolute top-1.5 right-1.5 w-4 h-4 bg-red-500 rounded-full border-2 border-white text-[10px] text-white font-bold flex items-center justify-center animate-pulse">
{notifications.length}
</span>
)}
<Bell size={20} />
{notifications.length > 0 && (
<span className="absolute top-1.5 right-1.5 w-4 h-4 bg-red-500 rounded-full border-2 border-white text-[10px] text-white font-bold flex items-center justify-center animate-pulse">
{notifications.length}
</span>
)}
</button>
<AnimatePresence>
{showNotifications && (
<motion.div
initial={{ opacity: 0, y: 10, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 5, scale: 0.95 }}
className="absolute right-0 top-full mt-2 w-80 bg-white/95 backdrop-blur-xl border border-[#e2e8f0] rounded-2xl shadow-2xl overflow-hidden z-50"
>
<div className="p-4 border-b border-[#e2e8f0] flex items-center justify-between bg-gray-50/50">
<h3 className="text-sm font-black text-slate-800 uppercase tracking-wider">Notifications</h3>
{notifications.length > 0 && (
<button
onClick={markAllAsRead}
className="text-[10px] font-bold text-indigo-600 hover:text-indigo-800 uppercase tracking-tighter"
>
Clear All
</button>
)}
</div>
{showNotifications && (
<motion.div
initial={{ opacity: 0, y: 10, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 5, scale: 0.95 }}
className="absolute right-0 top-full mt-2 w-80 bg-white/95 backdrop-blur-xl border border-[#e2e8f0] rounded-2xl shadow-2xl overflow-hidden z-50"
>
<div className="p-4 border-b border-[#e2e8f0] flex items-center justify-between bg-gray-50/50">
<h3 className="text-sm font-black text-slate-800 uppercase tracking-wider">Notifications</h3>
{notifications.length > 0 && (
<button
onClick={markAllAsRead}
className="text-[10px] font-bold text-indigo-600 hover:text-indigo-800 uppercase tracking-tighter"
>
Clear All
</button>
)}
</div>
<div className="max-h-[400px] overflow-y-auto custom-scrollbar">
{notifications.length > 0 ? (
notifications.map((notif) => (
<button
key={notif.id}
onClick={() => handleNotificationClick(notif)}
className="w-full p-4 border-b border-gray-50 hover:bg-gray-50/80 transition-colors flex gap-3 text-left group"
>
<div className="mt-1 shrink-0 p-2 bg-white rounded-xl shadow-sm border border-gray-100 group-hover:border-indigo-100 transition-colors">
{getNotificationIcon(notif.type)}
</div>
<div className="flex-1 min-w-0">
<div className="text-xs font-black text-slate-800 mb-0.5 truncate">{notif.title}</div>
<div className="text-[11px] text-slate-500 font-medium leading-relaxed line-clamp-2">{notif.message}</div>
<div className="mt-2 text-[9px] font-bold text-slate-400 uppercase tracking-widest">
{new Date(notif.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</div>
</div>
</button>
))
) : (
<div className="p-12 text-center">
<div className="w-12 h-12 bg-slate-50 rounded-full flex items-center justify-center mx-auto mb-4">
<Bell size={24} className="text-slate-200" />
</div>
<p className="text-xs font-bold text-slate-400 uppercase tracking-widest">All caught up!</p>
</div>
)}
<div className="max-h-[400px] overflow-y-auto custom-scrollbar">
{notifications.length > 0 ? (
notifications.map((notif) => (
<button
key={notif.id}
onClick={() => handleNotificationClick(notif)}
className="w-full p-4 border-b border-gray-50 hover:bg-gray-50/80 transition-colors flex gap-3 text-left group"
>
<div className="mt-1 shrink-0 p-2 bg-white rounded-xl shadow-sm border border-gray-100 group-hover:border-indigo-100 transition-colors">
{getNotificationIcon(notif.type)}
</div>
<div className="flex-1 min-w-0">
<div className="text-xs font-black text-slate-800 mb-0.5 truncate">{notif.title}</div>
<div className="text-[11px] text-slate-500 font-medium leading-relaxed line-clamp-2">{notif.message}</div>
<div className="mt-2 text-[9px] font-bold text-slate-400 uppercase tracking-widest">
{new Date(notif.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</div>
</div>
</button>
))
) : (
<div className="p-12 text-center">
<div className="w-12 h-12 bg-slate-50 rounded-full flex items-center justify-center mx-auto mb-4">
<Bell size={24} className="text-slate-200" />
</div>
</motion.div>
)}
<p className="text-xs font-bold text-slate-400 uppercase tracking-widest">All caught up!</p>
</div>
)}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
<Link to="/settings" className="p-2 text-[#64748b] hover:text-[#0f4475] hover:bg-indigo-50 rounded-lg transition-all">
<Link to="/settings" className="p-2 text-[#64748b] hover:text-[#003317] hover:bg-indigo-50 rounded-lg transition-all">
<Settings size={20} />
</Link>

View File

@@ -223,20 +223,20 @@ const Sidebar = () => {
<li key={item.title}>
{item.subMenu ? (
<div>
<button
<button
onClick={() => toggleMenu(item.title)}
title={`Expand/Collapse ${item.title}`}
className={cn(
"w-full flex items-center justify-between p-2.5 rounded-xl text-sm font-semibold transition-all duration-200 group",
openMenus.includes(item.title)
? "text-[#0f4475] bg-[#0f4475]/5"
? "text-[#003317] bg-[#003317]/5"
: "text-[#475569] hover:bg-gray-50 hover:text-[#1e293b]"
)}
>
<div className="flex items-center gap-3">
<item.icon size={18} strokeWidth={2} className={cn(
"transition-colors",
openMenus.includes(item.title) ? "text-[#0f4475]" : "text-[#64748b] group-hover:text-[#475569]"
openMenus.includes(item.title) ? "text-[#003317]" : "text-[#64748b] group-hover:text-[#475569]"
)} />
<span className="tracking-tight">{item.title}</span>
</div>
@@ -265,13 +265,13 @@ const Sidebar = () => {
onClick={() => toggleNestedMenu(sub.title)}
className={cn(
"w-full flex items-center justify-between py-2 px-4 rounded-lg text-[13px] font-medium transition-all group",
openNestedMenus.includes(sub.title) ? "text-[#0f4475] bg-[#0f4475]/5" : "text-[#64748b] hover:text-[#1e293b]"
openNestedMenus.includes(sub.title) ? "text-[#003317] bg-[#003317]/5" : "text-[#64748b] hover:text-[#1e293b]"
)}
>
<div className="flex items-center gap-3">
<div className={cn(
"w-1.5 h-1.5 rounded-full",
openNestedMenus.includes(sub.title) ? "bg-[#0f4475]" : "bg-gray-300"
openNestedMenus.includes(sub.title) ? "bg-[#003317]" : "bg-gray-300"
)} />
<span>{sub.title}</span>
</div>
@@ -295,7 +295,7 @@ const Sidebar = () => {
to={nested.path!}
className={({ isActive }) => cn(
"flex items-center gap-3 py-1.5 px-4 rounded-lg text-[12px] font-medium transition-all",
isActive ? "text-[#0f4475] bg-white shadow-sm" : "text-[#64748b] hover:text-[#1e293b]"
isActive ? "text-[#003317] bg-white shadow-sm" : "text-[#64748b] hover:text-[#1e293b]"
)}
>
<div className="w-1 h-1 rounded-full bg-gray-300" />
@@ -314,7 +314,7 @@ const Sidebar = () => {
className={({ isActive }) => cn(
"flex items-center gap-3 py-2 px-4 rounded-lg text-[13px] font-medium transition-all group relative",
isActive
? "text-white bg-[#0f4475] shadow-md shadow-[#0f4475]/10"
? "text-white bg-[#003317] shadow-md shadow-[#003317]/10"
: "text-[#64748b] hover:text-[#1e293b] hover:translate-x-1"
)}
>
@@ -322,13 +322,13 @@ const Sidebar = () => {
<>
<div className={cn(
"w-1.5 h-1.5 rounded-full transition-all duration-300",
isActive ? "bg-[#0f4475] scale-125" : "bg-gray-300 group-hover:bg-gray-400"
isActive ? "bg-[#003317] scale-125" : "bg-gray-300 group-hover:bg-gray-400"
)} />
<span>{sub.title}</span>
{isActive && (
<motion.div
layoutId="activeSubMenu"
className="absolute left-[-1.5px] top-1/4 bottom-1/4 w-[3px] bg-[#0f4475] rounded-full"
className="absolute left-[-1.5px] top-1/4 bottom-1/4 w-[3px] bg-[#003317] rounded-full"
/>
)}
</>
@@ -348,7 +348,7 @@ const Sidebar = () => {
className={({ isActive }) => cn(
"flex items-center gap-3 p-2.5 rounded-xl text-sm font-semibold transition-all duration-200 group",
isActive
? "bg-[#0f4475] text-white shadow-lg shadow-[#0f4475]/15"
? "bg-[#003317] text-white shadow-lg shadow-[#003317]/15"
: "text-[#475569] hover:bg-gray-50 hover:text-[#1e293b]"
)}
>
@@ -388,7 +388,8 @@ const Sidebar = () => {
</button>
</div>
<style dangerouslySetInnerHTML={{ __html: `
<style dangerouslySetInnerHTML={{
__html: `
.custom-scrollbar::-webkit-scrollbar { width: 4px; }
.custom-scrollbar::-webkit-scrollbar-track { background: transparent; }
.custom-scrollbar::-webkit-scrollbar-thumb { background: #e2e8f0; border-radius: 10px; }

View File

@@ -2,466 +2,466 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Target,
ChevronRight,
ShoppingBag,
ArrowRight,
Wallet,
BarChart3,
MessageSquare,
Flame,
TrendingUp
Target,
ChevronRight,
ShoppingBag,
ArrowRight,
Wallet,
BarChart3,
MessageSquare,
Flame,
TrendingUp
} from 'lucide-react';
import {
AreaChart,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
PieChart,
Pie,
Cell
AreaChart,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
PieChart,
Pie,
Cell
} from 'recharts';
import { motion } from 'framer-motion';
const Dashboard = () => {
const navigate = useNavigate();
const [activeTab, setActiveTab] = useState('Sales');
const [timeRange, setTimeRange] = useState('Today');
const [customDates, setCustomDates] = useState({ from: '', to: '' });
const [data, setData] = useState<any>(null);
const [isLoading, setIsLoading] = useState(true);
const navigate = useNavigate();
const [activeTab, setActiveTab] = useState('Sales');
const [timeRange, setTimeRange] = useState('Today');
const [customDates, setCustomDates] = useState({ from: '', to: '' });
const [data, setData] = useState<any>(null);
const [isLoading, setIsLoading] = useState(true);
const toLocalISOString = (date: Date) => {
const tzo = -date.getTimezoneOffset(),
dif = tzo >= 0 ? '+' : '-',
pad = (num: number) => {
const toLocalISOString = (date: Date) => {
const tzo = -date.getTimezoneOffset(),
dif = tzo >= 0 ? '+' : '-',
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());
};
};
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();
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);
// 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 fetchData = async () => {
try {
setIsLoading(true);
const range = getRangeDates(timeRange);
let url = '/api/dashboard/stats';
if (range) {
const params = new URLSearchParams();
params.append('from', range.from);
params.append('to', range.to);
url += `?${params.toString()}`;
}
console.log('[DASHBOARD-TRACE] Fetching stats from:', url);
const response = await apiFetch(url);
if (response.ok) {
const result = await response.json();
setData(result);
}
} catch (error) {
console.error('Error fetching dashboard data:', error);
} finally {
setIsLoading(false);
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);
}
};
fetchData();
}, [timeRange, customDates]);
return { from: toLocalISOString(start), to: toLocalISOString(end) };
};
if (isLoading || !data) {
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-[#0f4475] font-black uppercase tracking-widest text-sm"
>
Loading Business Intelligence...
</motion.div>
</div>
);
}
useEffect(() => {
const fetchData = async () => {
try {
setIsLoading(true);
const range = getRangeDates(timeRange);
let url = '/api/dashboard/stats';
const { stats, storeOverview, hourlySales, insights, trendingItems } = data;
if (range) {
const params = new URLSearchParams();
params.append('from', range.from);
params.append('to', range.to);
url += `?${params.toString()}`;
}
const pieData = [
{ name: 'Full Payment', value: stats.periodRevenue, color: '#8b5cf6' },
{ name: 'Credit', value: 0, color: '#fbbf24' }
];
console.log('[DASHBOARD-TRACE] Fetching stats from:', url);
const response = await apiFetch(url);
if (response.ok) {
const result = await response.json();
setData(result);
}
} catch (error) {
console.error('Error fetching dashboard data:', error);
} finally {
setIsLoading(false);
}
};
fetchData();
}, [timeRange, customDates]);
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="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} />
</button>
<h2 className="text-xs font-black text-[#0f4475] uppercase tracking-widest">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>
<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>
The default time settings for the merchant view are from 12:00 AM to 11:59 PM.
</p>
</div>
if (isLoading || !data) {
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"
>
Loading Business Intelligence...
</motion.div>
</div>
);
}
<div className="flex items-center gap-3">
<button
onClick={() => navigate('/feedback')}
className="flex items-center gap-2 bg-amber-50 text-amber-600 px-4 py-2 rounded-xl text-xs font-black uppercase tracking-widest hover:bg-amber-500 hover:text-white transition-all shadow-sm active:scale-95"
>
<MessageSquare size={16} />
Customer Feedback
</button>
const { stats, storeOverview, hourlySales, insights, trendingItems } = data;
<button
onClick={() => navigate('/reports')}
className="flex items-center gap-2 bg-indigo-50 text-indigo-600 px-4 py-2 rounded-xl text-xs font-black uppercase tracking-widest hover:bg-indigo-600 hover:text-white transition-all shadow-sm active:scale-95"
>
<BarChart3 size={16} />
View Full Report
</button>
const pieData = [
{ name: 'Full Payment', value: stats.periodRevenue, color: '#8b5cf6' },
{ name: 'Credit', value: 0, color: '#fbbf24' }
];
</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={`View ${tab} analysis and trends`}
onClick={() => setActiveTab(tab)}
className={`pb-4 text-[11px] font-black uppercase tracking-widest transition-all relative ${activeTab === tab ? 'text-[#0f4475]' : 'text-slate-400 hover:text-slate-600'}`}
>
{tab}
{activeTab === tab && (
<motion.div layoutId="tabLineMain" className="absolute bottom-0 left-0 right-0 h-0.5 bg-[#0f4475]" />
)}
</button>
))}
</div>
<div className="flex items-center justify-end mb-4 gap-4">
<div className="flex items-center gap-2">
<div className="w-2.5 h-2.5 rounded-full border-2 border-[#0f4475]/30 bg-[#0f4475]/10" />
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">{timeRange}'s Revenue</span>
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="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} />
</button>
<h2 className="text-xs font-black text-[#003317] uppercase tracking-widest">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>
<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>
The default time settings for the merchant view are from 12:00 AM to 11:59 PM.
</p>
</div>
<div className="h-[280px] w-full mt-4">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={hourlySales}>
<defs>
<linearGradient id="colorSalesMain" 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 ? `₹${v/1000}k` : `₹${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(#colorSalesMain)" />
</AreaChart>
</ResponsiveContainer>
</div>
</div>
<div className="flex items-center gap-3">
<button
onClick={() => navigate('/feedback')}
className="flex items-center gap-2 bg-amber-50 text-amber-600 px-4 py-2 rounded-xl text-xs font-black uppercase tracking-widest hover:bg-amber-500 hover:text-white transition-all shadow-sm active:scale-95"
>
<MessageSquare size={16} />
Customer Feedback
</button>
{/* 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="flex items-center justify-between mb-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>
<Target size={14} className="text-slate-300" />
</div>
</div>
<button
onClick={() => navigate('/reports')}
className="flex items-center gap-2 bg-indigo-50 text-indigo-600 px-4 py-2 rounded-xl text-xs font-black uppercase tracking-widest hover:bg-indigo-600 hover:text-white transition-all shadow-sm active:scale-95"
>
<BarChart3 size={16} />
View Full Report
</button>
<div className="relative h-[280px] flex flex-col items-center justify-center">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={pieData}
cx="50%"
cy="50%"
innerRadius={75}
outerRadius={100}
paddingAngle={0}
dataKey="value"
startAngle={210}
endAngle={-30}
>
{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">₹{(stats.periodRevenue || 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-[8px] font-bold text-slate-300 uppercase tracking-widest mt-0.5">Total: ₹{(stats.totalSales || 0).toLocaleString()}</p>
</div>
<div className="flex gap-6 mt-2">
{pieData.map(item => (
<div key={item.name} className="flex items-center gap-2">
<div className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: item.color }} />
<span className="text-[9px] font-black text-slate-500 uppercase tracking-widest font-black">{item.name}</span>
</div>
))}
</div>
</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 => (
{/* 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={range}
title={`Analyze data from ${range.toLowerCase()}`}
onClick={() => setTimeRange(range)}
className={`whitespace-nowrap px-3 py-2 text-[10px] font-black uppercase tracking-widest transition-all rounded-xl ${timeRange === range ? 'text-white bg-[#0f4475] shadow-md shadow-[#0f4475]/20' : 'text-slate-400 hover:text-slate-600'}`}
key={tab}
title={`View ${tab} analysis and trends`}
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'}`}
>
{range}
{tab}
{activeTab === tab && (
<motion.div layoutId="tabLineMain" 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-[#0f4475]"
/>
<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-[#0f4475]"
/>
</div>
)}
</div>
{/* 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-[#0f4475]/30 transition-all cursor-pointer">
<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 Orders</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>
{/* Expenses Card */}
<div title="Track operational expenditures and overhead" className="bg-white p-6 rounded-[24px] border border-slate-100 shadow-sm h-[130px] flex flex-col justify-between group hover:border-[#0f4475]/30 transition-all cursor-pointer">
<div className="flex items-center gap-3">
<div className="p-2 bg-slate-50 text-[#0f4475] rounded-xl shadow-sm group-hover:bg-[#0f4475]/10 transition-colors">
<Wallet size={20} />
</div>
<h4 className="text-[11px] font-black text-slate-400 uppercase tracking-widest leading-none">Total Expenses</h4>
</div>
<div className="text-center pb-2">
<h2 className="text-3xl font-black text-slate-800 tracking-tighter">₹{stats.periodExpenses.toLocaleString()}</h2>
</div>
</div>
</div>
</div>
{/* Trending Items Section */}
<section>
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<div className="p-2 bg-rose-50 text-rose-500 rounded-xl">
<Flame size={20} />
<div className="flex items-center justify-end mb-4 gap-4">
<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" />
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">{timeRange}'s Revenue</span>
</div>
</div>
<div className="h-[280px] w-full mt-4">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={hourlySales}>
<defs>
<linearGradient id="colorSalesMain" 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 ? `₹${v / 1000}k` : `₹${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(#colorSalesMain)" />
</AreaChart>
</ResponsiveContainer>
</div>
<h3 className="text-sm font-black text-slate-800 tracking-tight uppercase tracking-[0.1em]">Best Selling Food</h3>
</div>
<div className="text-[10px] font-black text-slate-400 uppercase tracking-widest flex items-center gap-2">
Performance Index <TrendingUp size={12} className="text-emerald-500" />
{/* 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="flex items-center justify-between mb-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>
<Target size={14} className="text-slate-300" />
</div>
</div>
<div className="relative h-[280px] flex flex-col items-center justify-center">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={pieData}
cx="50%"
cy="50%"
innerRadius={75}
outerRadius={100}
paddingAngle={0}
dataKey="value"
startAngle={210}
endAngle={-30}
>
{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">₹{(stats.periodRevenue || 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-[8px] font-bold text-slate-300 uppercase tracking-widest mt-0.5">Total: ₹{(stats.totalSales || 0).toLocaleString()}</p>
</div>
<div className="flex gap-6 mt-2">
{pieData.map(item => (
<div key={item.name} className="flex items-center gap-2">
<div className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: item.color }} />
<span className="text-[9px] font-black text-slate-500 uppercase tracking-widest font-black">{item.name}</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={`Analyze data from ${range.toLowerCase()}`}
onClick={() => setTimeRange(range)}
className={`whitespace-nowrap px-3 py-2 text-[10px] font-black uppercase tracking-widest transition-all rounded-xl ${timeRange === range ? 'text-white bg-[#003317] shadow-md shadow-[#003317]/20' : 'text-slate-400 hover:text-slate-600'}`}
>
{range}
</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 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 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 Orders</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>
{/* Expenses Card */}
<div title="Track operational expenditures and overhead" className="bg-white p-6 rounded-[24px] border border-slate-100 shadow-sm h-[130px] flex flex-col justify-between group hover:border-[#003317]/30 transition-all cursor-pointer">
<div className="flex items-center gap-3">
<div className="p-2 bg-slate-50 text-[#003317] rounded-xl shadow-sm group-hover:bg-[#003317]/10 transition-colors">
<Wallet size={20} />
</div>
<h4 className="text-[11px] font-black text-slate-400 uppercase tracking-widest leading-none">Total Expenses</h4>
</div>
<div className="text-center pb-2">
<h2 className="text-3xl font-black text-slate-800 tracking-tighter">₹{stats.periodExpenses.toLocaleString()}</h2>
</div>
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{trendingItems.map((item: any, idx: number) => (
<motion.div
key={idx}
whileHover={{ y: -5 }}
className="bg-white p-5 rounded-[28px] border border-slate-100 shadow-sm hover:shadow-xl hover:border-indigo-500/20 transition-all group overflow-hidden relative cursor-default"
>
<div className="absolute top-4 right-4 bg-white/80 backdrop-blur-md px-2.5 py-1 rounded-lg text-[10px] font-black text-slate-900 border border-slate-100 z-10 shadow-sm">
TOP #{idx+1}
{/* Trending Items Section */}
<section>
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<div className="p-2 bg-rose-50 text-rose-500 rounded-xl">
<Flame size={20} />
</div>
<div className="w-full h-32 rounded-2xl mb-5 overflow-hidden bg-slate-50 flex items-center justify-center">
{item.imageUrl ? (
<img
<h3 className="text-sm font-black text-slate-800 tracking-tight uppercase tracking-[0.1em]">Best Selling Food</h3>
</div>
<div className="text-[10px] font-black text-slate-400 uppercase tracking-widest flex items-center gap-2">
Performance Index <TrendingUp size={12} className="text-emerald-500" />
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{trendingItems.map((item: any, idx: number) => (
<motion.div
key={idx}
whileHover={{ y: -5 }}
className="bg-white p-5 rounded-[28px] border border-slate-100 shadow-sm hover:shadow-xl hover:border-indigo-500/20 transition-all group overflow-hidden relative cursor-default"
>
<div className="absolute top-4 right-4 bg-white/80 backdrop-blur-md px-2.5 py-1 rounded-lg text-[10px] font-black text-slate-900 border border-slate-100 z-10 shadow-sm">
TOP #{idx + 1}
</div>
<div className="w-full h-32 rounded-2xl mb-5 overflow-hidden bg-slate-50 flex items-center justify-center">
{item.imageUrl ? (
<img
src={item.imageUrl}
alt={item.name}
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-700 opacity-90 group-hover:opacity-100"
/>
) : (
<div className="w-full h-full bg-slate-50/50" />
)}
</div>
<div className="flex justify-between items-start mb-2">
<div>
<h4 className="text-sm font-black text-slate-800 leading-tight mb-1 group-hover:text-indigo-600 transition-colors uppercase tracking-tight">{item.name}</h4>
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">{item.category}</p>
/>
) : (
<div className="w-full h-full bg-slate-50/50" />
)}
</div>
<div className="flex justify-between items-start mb-2">
<div>
<h4 className="text-sm font-black text-slate-800 leading-tight mb-1 group-hover:text-indigo-600 transition-colors uppercase tracking-tight">{item.name}</h4>
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">{item.category}</p>
</div>
</div>
<div className="flex items-center justify-between mt-4 pt-4 border-t border-slate-50">
<div className="text-[10px] text-slate-400 font-black uppercase tracking-tight">Orders Count</div>
<div className="text-lg font-black text-indigo-900 tracking-tighter">{item.orderCount} <span className="text-[10px] font-bold text-slate-300">Unit(s)</span></div>
</div>
</div>
<div className="flex items-center justify-between mt-4 pt-4 border-t border-slate-50">
<div className="text-[10px] text-slate-400 font-black uppercase tracking-tight">Orders Count</div>
<div className="text-lg font-black text-indigo-900 tracking-tighter">{item.orderCount} <span className="text-[10px] font-bold text-slate-300">Unit(s)</span></div>
</div>
</motion.div>
))}
</div>
</section>
{/* Bottom Section */}
<div className="grid grid-cols-12 gap-8 mt-4 pb-12">
{/* Store Insights */}
<div className="col-span-12 lg:col-span-5 bg-white rounded-[32px] border border-slate-100 shadow-sm p-8 flex flex-col">
<h3 className="text-sm font-black text-slate-800 tracking-tight mb-8 uppercase tracking-widest">Business Intelligence</h3>
<div className="space-y-4 flex-1">
{insights.map((insight: any, idx: number) => (
<motion.div
initial={{ x: -20, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
transition={{ delay: idx * 0.1 }}
key={idx}
className={`p-5 rounded-2xl border transition-all cursor-default ${insight.color || 'border-slate-100 bg-slate-50/50 hover:bg-white hover:shadow-md hover:border-transparent'}`}
>
<p className="text-[11px] font-black text-slate-600 leading-relaxed uppercase tracking-tight">
{(insight.text || insight).replace(/R(?=[0-9])/g, '')}
</p>
</motion.div>
))}
</div>
<div className="mt-8">
<button title="Refresh and sync comprehensive business insights" className="w-full py-3 border border-dashed border-slate-200 rounded-2xl text-[10px] font-black text-slate-400 uppercase tracking-widest hover:border-[#0f4475]/30 hover:text-[#0f4475] transition-all">
Generate More Insights
</button>
</div>
</div>
</section>
{/* Store Overview */}
<div className="col-span-12 lg:col-span-7 bg-white rounded-[32px] border border-slate-100 shadow-sm p-8">
<div className="flex items-center justify-between mb-8">
<h3 className="text-sm font-black text-slate-800 tracking-tight uppercase tracking-widest">Store Performance</h3>
<button title="View detailed metrics for all geographic locations" className="text-[10px] font-black text-[#0f4475] uppercase tracking-widest hover:underline">View All Locations</button>
{/* Bottom Section */}
<div className="grid grid-cols-12 gap-8 mt-4 pb-12">
{/* Store Insights */}
<div className="col-span-12 lg:col-span-5 bg-white rounded-[32px] border border-slate-100 shadow-sm p-8 flex flex-col">
<h3 className="text-sm font-black text-slate-800 tracking-tight mb-8 uppercase tracking-widest">Business Intelligence</h3>
<div className="space-y-4 flex-1">
{insights.map((insight: any, idx: number) => (
<motion.div
initial={{ x: -20, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
transition={{ delay: idx * 0.1 }}
key={idx}
className={`p-5 rounded-2xl border transition-all cursor-default ${insight.color || 'border-slate-100 bg-slate-50/50 hover:bg-white hover:shadow-md hover:border-transparent'}`}
>
<p className="text-[11px] font-black text-slate-600 leading-relaxed uppercase tracking-tight">
{(insight.text || insight).replace(/R(?=[0-9])/g, '')}
</p>
</motion.div>
))}
</div>
<div className="mt-8">
<button title="Refresh and sync comprehensive business insights" className="w-full py-3 border border-dashed border-slate-200 rounded-2xl text-[10px] font-black text-slate-400 uppercase tracking-widest hover:border-[#003317]/30 hover:text-[#003317] transition-all">
Generate More Insights
</button>
</div>
</div>
<div className="space-y-6">
{(storeOverview.length > 0 ? storeOverview : [{name: 'No active sales currently', sale: 0, orders: 0, taxes: 0, purchase: 0}]).map((store: any, idx: number) => (
<div key={idx} className="p-6 rounded-[28px] border border-slate-50 bg-slate-50/30 group hover:bg-white hover:shadow-lg hover:border-transparent transition-all border-l-4 border-l-[#0f4475]/30">
<p className="text-xs font-black text-slate-800 mb-6 uppercase tracking-widest flex items-center justify-between">
{store.name}
<ArrowRight size={14} className="opacity-0 group-hover:opacity-100 transition-opacity text-[#0f4475]" />
</p>
<div className="grid grid-cols-4 gap-4">
<div className="space-y-1.5">
<p className="text-[9px] font-black text-rose-500 uppercase tracking-widest opacity-60">Gross Sale</p>
<p className="text-lg font-black text-slate-800 tracking-tighter">{Number(store.sale).toLocaleString()}</p>
</div>
<div className="space-y-1.5">
<p className="text-[9px] font-black text-blue-500 uppercase tracking-widest opacity-60">Volume</p>
<p className="text-lg font-black text-slate-800 tracking-tighter">{store.orders} Orders</p>
</div>
<div className="space-y-1.5">
<p className="text-[9px] font-black text-indigo-500 uppercase tracking-widest opacity-60">Taxation</p>
<p className="text-lg font-black text-slate-800 tracking-tighter">{store.taxes}</p>
</div>
<div className="space-y-1.5 text-right">
<p className="text-[9px] font-black text-amber-500 uppercase tracking-widest opacity-60">Procurement</p>
<p className="text-lg font-black text-slate-800 tracking-tighter">{store.purchase}</p>
{/* Store Overview */}
<div className="col-span-12 lg:col-span-7 bg-white rounded-[32px] border border-slate-100 shadow-sm p-8">
<div className="flex items-center justify-between mb-8">
<h3 className="text-sm font-black text-slate-800 tracking-tight uppercase tracking-widest">Store Performance</h3>
<button title="View detailed metrics for all geographic locations" className="text-[10px] font-black text-[#003317] uppercase tracking-widest hover:underline">View All Locations</button>
</div>
<div className="space-y-6">
{(storeOverview.length > 0 ? storeOverview : [{ name: 'No active sales currently', sale: 0, orders: 0, taxes: 0, purchase: 0 }]).map((store: any, idx: number) => (
<div key={idx} className="p-6 rounded-[28px] border border-slate-50 bg-slate-50/30 group hover:bg-white hover:shadow-lg hover:border-transparent transition-all border-l-4 border-l-[#003317]/30">
<p className="text-xs font-black text-slate-800 mb-6 uppercase tracking-widest flex items-center justify-between">
{store.name}
<ArrowRight size={14} className="opacity-0 group-hover:opacity-100 transition-opacity text-[#003317]" />
</p>
<div className="grid grid-cols-4 gap-4">
<div className="space-y-1.5">
<p className="text-[9px] font-black text-rose-500 uppercase tracking-widest opacity-60">Gross Sale</p>
<p className="text-lg font-black text-slate-800 tracking-tighter">{Number(store.sale).toLocaleString()}</p>
</div>
<div className="space-y-1.5">
<p className="text-[9px] font-black text-blue-500 uppercase tracking-widest opacity-60">Volume</p>
<p className="text-lg font-black text-slate-800 tracking-tighter">{store.orders} Orders</p>
</div>
<div className="space-y-1.5">
<p className="text-[9px] font-black text-indigo-500 uppercase tracking-widest opacity-60">Taxation</p>
<p className="text-lg font-black text-slate-800 tracking-tighter">{store.taxes}</p>
</div>
<div className="space-y-1.5 text-right">
<p className="text-[9px] font-black text-amber-500 uppercase tracking-widest opacity-60">Procurement</p>
<p className="text-lg font-black text-slate-800 tracking-tighter">{store.purchase}</p>
</div>
</div>
</div>
</div>
))}
</div>
))}
</div>
<div className="mt-10 flex justify-center">
<button title="Execute a granular comparison across different store cohorts" className="flex items-center gap-3 px-8 py-3 bg-[#0f4475] text-white text-[11px] font-black uppercase tracking-widest rounded-2xl shadow-xl shadow-[#0f4475]/20 hover:scale-105 transition-all group">
Detailed Comparison
<ArrowRight size={16} className="group-hover:translate-x-1 transition-transform" />
</button>
<div className="mt-10 flex justify-center">
<button title="Execute a granular comparison across different store cohorts" className="flex items-center gap-3 px-8 py-3 bg-[#003317] text-white text-[11px] font-black uppercase tracking-widest rounded-2xl shadow-xl shadow-[#003317]/20 hover:scale-105 transition-all group">
Detailed Comparison
<ArrowRight size={16} className="group-hover:translate-x-1 transition-transform" />
</button>
</div>
</div>
</div>
</div>
</div>
);
);
};
export default Dashboard;

View File

@@ -4,271 +4,270 @@ import { useNavigate } from 'react-router-dom';
import { motion } from 'framer-motion';
import { format } from 'date-fns';
import {
Filter,
ChevronRight,
ShoppingBag,
Package,
Wallet,
RefreshCw,
Clock,
LayoutGrid,
FileText,
Boxes,
Eye,
ArrowRight
Filter,
ChevronRight,
ShoppingBag,
Package,
Wallet,
RefreshCw,
Clock,
LayoutGrid,
FileText,
Boxes,
Eye,
ArrowRight
} from 'lucide-react';
import {
AreaChart,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer
AreaChart,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer
} from 'recharts';
interface IntentDashboardProps {
title: string;
title: string;
}
const StatCard = ({ icon: Icon, label, value, color }: any) => (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white p-5 rounded-2xl border border-[#e2e8f0] flex items-center gap-4 flex-1 shadow-sm hover:shadow-md transition-all"
>
<div className={`p-3 rounded-xl ${color} bg-opacity-10 text-${color.split('-')[1]}-600`}>
<Icon size={20} />
</div>
<div>
<p className="text-[11px] font-bold text-[#94a3b8] uppercase tracking-wider">{label}</p>
<h3 className="text-xl font-black text-[#1e293b]">{value}</h3>
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white p-5 rounded-2xl border border-[#e2e8f0] flex items-center gap-4 flex-1 shadow-sm hover:shadow-md transition-all"
>
<div className={`p-3 rounded-xl ${color} bg-opacity-10 text-${color.split('-')[1]}-600`}>
<Icon size={20} />
</div>
<div>
<p className="text-[11px] font-bold text-[#94a3b8] uppercase tracking-wider">{label}</p>
<h3 className="text-xl font-black text-[#1e293b]">{value}</h3>
</div>
</motion.div>
);
interface IntentStats {
openCount: number;
openItems: number;
payableAmount: number;
unbilledCount: number;
recentOrders: any[];
trend: any[];
openCount: number;
openItems: number;
payableAmount: number;
unbilledCount: number;
recentOrders: any[];
trend: any[];
}
const IntentDashboard: React.FC<IntentDashboardProps> = ({ title }) => {
const [stats, setStats] = useState<IntentStats | null>(null);
const [loading, setLoading] = useState(true);
const [filterTab, setFilterTab] = useState('All');
const navigate = useNavigate();
const [stats, setStats] = useState<IntentStats | null>(null);
const [loading, setLoading] = useState(true);
const [filterTab, setFilterTab] = useState('All');
const navigate = useNavigate();
useEffect(() => {
fetchStats();
}, [title]);
useEffect(() => {
fetchStats();
}, [title]);
const fetchStats = async () => {
setLoading(true);
try {
const response = await apiFetch(`http://${window.location.hostname}:8080/api/purchases/intent/summary`);
const data = await response.json();
setStats(data);
} catch (error) {
console.error('Error fetching intent stats:', error);
} finally {
setLoading(false);
}
};
const fetchStats = async () => {
setLoading(true);
try {
const response = await apiFetch(`http://${window.location.hostname}:8080/api/purchases/intent/summary`);
const data = await response.json();
setStats(data);
} catch (error) {
console.error('Error fetching intent stats:', error);
} finally {
setLoading(false);
}
};
const chartData = stats?.trend?.map(t => ({
const chartData = stats?.trend?.map(t => ({
date: format(new Date(t[0]), 'MMM dd'),
total: t[1],
open: t[1] * 0.4 // Sample logic for visual variety if status-specific trend isn't available
})) || [];
})) || [];
const statusBreakdown = [
{ label: 'OPEN', value: stats?.openCount || 0 },
{ label: 'CLOSE', value: stats?.recentOrders?.filter(o => o.status === 'CLOSE').length || 0 },
{ label: 'ACKNOWLEDGE', value: 0 },
{ label: 'RECEIVED', value: stats?.unbilledCount || 0 }
];
return (
<div className="p-6 space-y-6 bg-[#f8fafc] min-h-screen font-inter">
{/* Top Header & Filter */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<h1 className="text-xl font-black text-[#0f4475] uppercase tracking-wider flex items-center gap-2">
<LayoutGrid size={20} />
{title}
</h1>
<div className="flex items-center gap-3 bg-white p-1.5 rounded-2xl border border-[#e2e8f0] shadow-sm">
<button className="flex items-center gap-2 bg-[#0f4475] text-white px-4 py-2 rounded-xl text-xs font-bold hover:bg-[#1e4e8c] transition-all shadow-md shadow-[#0f4475]/10">
<Filter size={14} /> Filter
</button>
<div className="flex items-center gap-3 px-3 border-l border-[#e2e8f0]">
<div className="flex items-center gap-2">
<span className="text-[10px] font-bold text-[#94a3b8] uppercase">Start:</span>
<span className="text-xs font-bold text-[#0f4475] bg-[#0f4475]/5 px-2.5 py-1 rounded-lg">2026-03-10</span>
</div>
<div className="flex items-center gap-2">
<span className="text-[10px] font-bold text-[#94a3b8] uppercase">End:</span>
<span className="text-xs font-bold text-[#0f4475] bg-[#0f4475]/5 px-2.5 py-1 rounded-lg">2026-04-10</span>
</div>
</div>
</div>
</div>
{/* Stats Grid */}
<div className="flex flex-wrap gap-4 relative">
{loading && <div className="absolute inset-0 bg-white/50 backdrop-blur-[1px] z-10 rounded-2xl flex items-center justify-center"><RefreshCw className="animate-spin text-[#0f4475]" size={24} /></div>}
<StatCard icon={Clock} label="Open" value={stats?.openCount || 0} color="bg-cyan-500" />
<StatCard icon={Boxes} label="Open Items" value={stats?.openItems || 0} color="bg-indigo-500" />
<StatCard icon={Wallet} label="Payable Amount" value={`${stats?.payableAmount?.toLocaleString() || 0}`} color="bg-amber-500" />
<StatCard icon={FileText} label="Unbilled" value={stats?.unbilledCount || 0} color="bg-rose-500" />
</div>
{/* Main Content Area */}
<div className="grid grid-cols-12 gap-6">
{/* Left Column: Chart & Breakdown */}
<div className="col-span-12 lg:col-span-8 space-y-6">
<div className="bg-white p-6 rounded-3xl border border-[#e2e8f0] shadow-sm relative overflow-hidden">
<div className="flex items-center justify-between mb-8">
<h3 className="text-sm font-black text-[#1e293b] flex items-center gap-2">
Orders in the last 30 days
<button onClick={fetchStats} className={`text-[#94a3b8] hover:text-[#0f4475] transition-colors ${loading ? 'animate-spin' : ''}`}><RefreshCw size={14} /></button>
</h3>
<div className="flex items-center gap-4">
{['Open', 'Closed', 'Total'].map((l, i) => (
<div key={l} className="flex items-center gap-1.5">
<div className={`w-2.5 h-2.5 rounded-full ${i === 0 ? 'bg-amber-500' : i === 1 ? 'bg-rose-500' : 'bg-[#0f4475]'}`} />
<span className="text-[10px] font-bold text-[#64748b]">{l}</span>
</div>
))}
const statusBreakdown = [
{ label: 'OPEN', value: stats?.openCount || 0 },
{ label: 'CLOSE', value: stats?.recentOrders?.filter(o => o.status === 'CLOSE').length || 0 },
{ label: 'ACKNOWLEDGE', value: 0 },
{ label: 'RECEIVED', value: stats?.unbilledCount || 0 }
];
return (
<div className="p-6 space-y-6 bg-[#f8fafc] min-h-screen font-inter">
{/* Top Header & Filter */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<h1 className="text-xl font-black text-[#003317] uppercase tracking-wider flex items-center gap-2">
<LayoutGrid size={20} />
{title}
</h1>
<div className="flex items-center gap-3 bg-white p-1.5 rounded-2xl border border-[#e2e8f0] shadow-sm">
<button className="flex items-center gap-2 bg-[#003317] text-white px-4 py-2 rounded-xl text-xs font-bold hover:bg-[#1e4e8c] transition-all shadow-md shadow-[#003317]/10">
<Filter size={14} /> Filter
</button>
<div className="flex items-center gap-3 px-3 border-l border-[#e2e8f0]">
<div className="flex items-center gap-2">
<span className="text-[10px] font-bold text-[#94a3b8] uppercase">Start:</span>
<span className="text-xs font-bold text-[#003317] bg-[#003317]/5 px-2.5 py-1 rounded-lg">2026-03-10</span>
</div>
</div>
<div className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#f1f5f9" />
<XAxis dataKey="date" axisLine={false} tickLine={false} tick={{fill: '#94a3b8', fontSize: 10, fontWeight: 700}} dy={10} />
<YAxis axisLine={false} tickLine={false} tick={{fill: '#94a3b8', fontSize: 10, fontWeight: 700}} />
<Tooltip
contentStyle={{ borderRadius: '16px', border: 'none', boxShadow: '0 10px 15px -3px rgba(0,0,0,0.1)' }}
itemStyle={{ fontWeight: 'bold' }}
/>
<Area type="monotone" dataKey="open" stroke="#f59e0b" fill="#f59e0b" fillOpacity={0.05} strokeWidth={2} />
<Area type="monotone" dataKey="total" stroke="#0f4475" fill="#0f4475" fillOpacity={0.05} strokeWidth={2} />
</AreaChart>
</ResponsiveContainer>
</div>
</div>
<div className="grid grid-cols-2 gap-6">
<div className="bg-white p-6 rounded-3xl border border-[#e2e8f0] shadow-sm">
<h3 className="text-sm font-black text-[#1e293b] mb-12">Top Items</h3>
<div className="flex flex-col items-center justify-center p-8 text-[#94a3b8]">
<Package size={48} className="opacity-10 mb-4" />
<p className="text-xs font-bold uppercase tracking-widest">No items found</p>
</div>
</div>
<div className="bg-white p-6 rounded-3xl border border-[#e2e8f0] shadow-sm">
<h3 className="text-sm font-black text-[#1e293b] mb-6">Status Breakdown</h3>
<div className="space-y-4">
{statusBreakdown.map(s => (
<div key={s.label} className="group border-b border-[#f1f5f9] last:border-0 pb-3 last:pb-0 flex items-center justify-between hover:bg-gray-50/50 rounded-lg p-1 transition-all cursor-pointer">
<span className="text-[11px] font-black text-[#64748b] tracking-wider">{s.label}</span>
<div className="flex items-center gap-3">
<span className="text-xs font-black text-[#1e293b]">{s.value}</span>
<button className="text-[#94a3b8] group-hover:text-[#0f4475] opacity-0 group-hover:opacity-100 transition-all"><ArrowRight size={14} /></button>
</div>
</div>
))}
<div className="flex items-center gap-2">
<span className="text-[10px] font-bold text-[#94a3b8] uppercase">End:</span>
<span className="text-xs font-bold text-[#003317] bg-[#003317]/5 px-2.5 py-1 rounded-lg">2026-04-10</span>
</div>
</div>
</div>
</div>
{/* Right Column: Recent & Links */}
<div className="col-span-12 lg:col-span-4 space-y-6">
<div className="bg-white p-6 rounded-3xl border border-[#e2e8f0] shadow-sm overflow-hidden relative">
{loading && <div className="absolute inset-0 bg-white/30 backdrop-blur-[1px] z-10" />}
<div className="flex items-center justify-between mb-6">
<h3 className="text-sm font-black text-[#1e293b]">Recent Orders</h3>
<button onClick={fetchStats} className={`text-[#38bdf8] p-1.5 hover:bg-sky-50 rounded-lg transition-all ${loading ? 'animate-spin' : ''}`}><RefreshCw size={16} /></button>
</div>
<div className="flex gap-2 mb-4 overflow-x-auto pb-2 scrollbar-hide">
{['All', 'Open', 'Billed', 'Received', 'Closed'].map((t) => (
<button
key={t}
onClick={() => setFilterTab(t)}
className={`whitespace-nowrap px-3 py-1.5 rounded-lg text-[10px] font-bold transition-all ${filterTab === t ? 'bg-[#0f4475] text-white shadow-md shadow-[#0f4475]/20' : 'bg-[#f8fafc] text-[#64748b] hover:bg-[#f1f5f9]'}`}
>
{t}
</button>
))}
</div>
<div className="space-y-3">
{(() => {
const filtered = stats?.recentOrders?.filter(o => {
if (filterTab === 'All') return true;
if (filterTab === 'Open') return o.status === 'OPEN';
if (filterTab === 'Billed') return o.status === 'BILLED';
if (filterTab === 'Received') return o.status === 'RECEIVED';
if (filterTab === 'Closed') return o.status === 'CLOSE' || o.status === 'CLOSED';
return true;
}) || [];
{/* Stats Grid */}
<div className="flex flex-wrap gap-4 relative">
{loading && <div className="absolute inset-0 bg-white/50 backdrop-blur-[1px] z-10 rounded-2xl flex items-center justify-center"><RefreshCw className="animate-spin text-[#003317]" size={24} /></div>}
<StatCard icon={Clock} label="Open" value={stats?.openCount || 0} color="bg-cyan-500" />
<StatCard icon={Boxes} label="Open Items" value={stats?.openItems || 0} color="bg-indigo-500" />
<StatCard icon={Wallet} label="Payable Amount" value={`${stats?.payableAmount?.toLocaleString() || 0}`} color="bg-amber-500" />
<StatCard icon={FileText} label="Unbilled" value={stats?.unbilledCount || 0} color="bg-rose-500" />
</div>
if (filtered.length === 0) {
return <div className="py-12 text-center text-[#94a3b8] text-xs font-bold italic">No {filterTab.toLowerCase()} orders</div>;
}
{/* Main Content Area */}
<div className="grid grid-cols-12 gap-6">
{/* Left Column: Chart & Breakdown */}
<div className="col-span-12 lg:col-span-8 space-y-6">
<div className="bg-white p-6 rounded-3xl border border-[#e2e8f0] shadow-sm relative overflow-hidden">
<div className="flex items-center justify-between mb-8">
<h3 className="text-sm font-black text-[#1e293b] flex items-center gap-2">
Orders in the last 30 days
<button onClick={fetchStats} className={`text-[#94a3b8] hover:text-[#003317] transition-colors ${loading ? 'animate-spin' : ''}`}><RefreshCw size={14} /></button>
</h3>
<div className="flex items-center gap-4">
{['Open', 'Closed', 'Total'].map((l, i) => (
<div key={l} className="flex items-center gap-1.5">
<div className={`w-2.5 h-2.5 rounded-full ${i === 0 ? 'bg-amber-500' : i === 1 ? 'bg-rose-500' : 'bg-[#003317]'}`} />
<span className="text-[10px] font-bold text-[#64748b]">{l}</span>
</div>
))}
</div>
</div>
<div className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#f1f5f9" />
<XAxis dataKey="date" axisLine={false} tickLine={false} tick={{ fill: '#94a3b8', fontSize: 10, fontWeight: 700 }} dy={10} />
<YAxis axisLine={false} tickLine={false} tick={{ fill: '#94a3b8', fontSize: 10, fontWeight: 700 }} />
<Tooltip
contentStyle={{ borderRadius: '16px', border: 'none', boxShadow: '0 10px 15px -3px rgba(0,0,0,0.1)' }}
itemStyle={{ fontWeight: 'bold' }}
/>
<Area type="monotone" dataKey="open" stroke="#f59e0b" fill="#f59e0b" fillOpacity={0.05} strokeWidth={2} />
<Area type="monotone" dataKey="total" stroke="#003317" fill="#003317" fillOpacity={0.05} strokeWidth={2} />
</AreaChart>
</ResponsiveContainer>
</div>
</div>
return filtered.map(o => (
<div key={o.id} className="p-3 bg-[#f8fafc] border border-[#f1f5f9] rounded-xl hover:shadow-md transition-all cursor-pointer group">
<div className="flex justify-between items-start mb-1">
<span className="text-[11px] font-black text-[#1e293b]">#{o.purchaseId || o.id}</span>
<span className="text-[11px] font-black text-[#1e293b]">{o.amount?.toLocaleString()}</span>
</div>
<div className="flex justify-between items-end">
<span className="text-[10px] font-semibold text-[#94a3b8]">{o.date ? format(new Date(o.date), 'MMM dd, yyyy HH:mm') : 'N/A'}</span>
<span className={`text-[9px] font-black px-1.5 py-0.5 rounded uppercase ${
o.status === 'CLOSE' || o.status === 'CLOSED' ? 'text-rose-600 bg-rose-50' :
o.status === 'OPEN' ? 'text-amber-600 bg-amber-50' :
o.status === 'BILLED' ? 'text-indigo-600 bg-indigo-50' :
'text-emerald-700 bg-emerald-100'
}`}>{o.status}</span>
</div>
</div>
));
})()}
<div className="grid grid-cols-2 gap-6">
<div className="bg-white p-6 rounded-3xl border border-[#e2e8f0] shadow-sm">
<h3 className="text-sm font-black text-[#1e293b] mb-12">Top Items</h3>
<div className="flex flex-col items-center justify-center p-8 text-[#94a3b8]">
<Package size={48} className="opacity-10 mb-4" />
<p className="text-xs font-bold uppercase tracking-widest">No items found</p>
</div>
</div>
<div className="bg-white p-6 rounded-3xl border border-[#e2e8f0] shadow-sm">
<h3 className="text-sm font-black text-[#1e293b] mb-6">Status Breakdown</h3>
<div className="space-y-4">
{statusBreakdown.map(s => (
<div key={s.label} className="group border-b border-[#f1f5f9] last:border-0 pb-3 last:pb-0 flex items-center justify-between hover:bg-gray-50/50 rounded-lg p-1 transition-all cursor-pointer">
<span className="text-[11px] font-black text-[#64748b] tracking-wider">{s.label}</span>
<div className="flex items-center gap-3">
<span className="text-xs font-black text-[#1e293b]">{s.value}</span>
<button className="text-[#94a3b8] group-hover:text-[#003317] opacity-0 group-hover:opacity-100 transition-all"><ArrowRight size={14} /></button>
</div>
</div>
))}
</div>
</div>
</div>
</div>
<div className="bg-white p-6 rounded-3xl border border-[#e2e8f0] shadow-sm">
<h3 className="text-sm font-black text-[#1e293b] mb-4">Quick Links</h3>
<div className="space-y-2">
{[
{ label: 'Inventory', icon: Boxes, path: '/inventory/products' },
{ label: 'Raw Materials', icon: Package, path: '/inventory/base' },
{ label: 'Draft Orders', icon: FileText, path: '/purchases/orders' }
].map(l => (
<button
key={l.label}
onClick={() => navigate(l.path)}
className="w-full flex items-center justify-between p-3.5 bg-[#f8fafc] border border-[#f1f5f9] rounded-xl hover:bg-gray-50 transition-all group"
>
<div className="flex items-center gap-3">
<div className="p-2 bg-white rounded-lg shadow-sm text-[#94a3b8] group-hover:text-[#0f4475] transition-colors">
<l.icon size={16} />
</div>
<span className="text-xs font-bold text-[#64748b]">{l.label}</span>
</div>
<ChevronRight size={14} className="text-[#cbd5e1] group-hover:text-[#0f4475] transition-all" />
</button>
))}
{/* Right Column: Recent & Links */}
<div className="col-span-12 lg:col-span-4 space-y-6">
<div className="bg-white p-6 rounded-3xl border border-[#e2e8f0] shadow-sm overflow-hidden relative">
{loading && <div className="absolute inset-0 bg-white/30 backdrop-blur-[1px] z-10" />}
<div className="flex items-center justify-between mb-6">
<h3 className="text-sm font-black text-[#1e293b]">Recent Orders</h3>
<button onClick={fetchStats} className={`text-[#38bdf8] p-1.5 hover:bg-sky-50 rounded-lg transition-all ${loading ? 'animate-spin' : ''}`}><RefreshCw size={16} /></button>
</div>
<div className="flex gap-2 mb-4 overflow-x-auto pb-2 scrollbar-hide">
{['All', 'Open', 'Billed', 'Received', 'Closed'].map((t) => (
<button
key={t}
onClick={() => setFilterTab(t)}
className={`whitespace-nowrap px-3 py-1.5 rounded-lg text-[10px] font-bold transition-all ${filterTab === t ? 'bg-[#003317] text-white shadow-md shadow-[#003317]/20' : 'bg-[#f8fafc] text-[#64748b] hover:bg-[#f1f5f9]'}`}
>
{t}
</button>
))}
</div>
<div className="space-y-3">
{(() => {
const filtered = stats?.recentOrders?.filter(o => {
if (filterTab === 'All') return true;
if (filterTab === 'Open') return o.status === 'OPEN';
if (filterTab === 'Billed') return o.status === 'BILLED';
if (filterTab === 'Received') return o.status === 'RECEIVED';
if (filterTab === 'Closed') return o.status === 'CLOSE' || o.status === 'CLOSED';
return true;
}) || [];
if (filtered.length === 0) {
return <div className="py-12 text-center text-[#94a3b8] text-xs font-bold italic">No {filterTab.toLowerCase()} orders</div>;
}
return filtered.map(o => (
<div key={o.id} className="p-3 bg-[#f8fafc] border border-[#f1f5f9] rounded-xl hover:shadow-md transition-all cursor-pointer group">
<div className="flex justify-between items-start mb-1">
<span className="text-[11px] font-black text-[#1e293b]">#{o.purchaseId || o.id}</span>
<span className="text-[11px] font-black text-[#1e293b]">{o.amount?.toLocaleString()}</span>
</div>
<div className="flex justify-between items-end">
<span className="text-[10px] font-semibold text-[#94a3b8]">{o.date ? format(new Date(o.date), 'MMM dd, yyyy HH:mm') : 'N/A'}</span>
<span className={`text-[9px] font-black px-1.5 py-0.5 rounded uppercase ${o.status === 'CLOSE' || o.status === 'CLOSED' ? 'text-rose-600 bg-rose-50' :
o.status === 'OPEN' ? 'text-amber-600 bg-amber-50' :
o.status === 'BILLED' ? 'text-indigo-600 bg-indigo-50' :
'text-emerald-700 bg-emerald-100'
}`}>{o.status}</span>
</div>
</div>
));
})()}
</div>
</div>
<div className="bg-white p-6 rounded-3xl border border-[#e2e8f0] shadow-sm">
<h3 className="text-sm font-black text-[#1e293b] mb-4">Quick Links</h3>
<div className="space-y-2">
{[
{ label: 'Inventory', icon: Boxes, path: '/inventory/products' },
{ label: 'Raw Materials', icon: Package, path: '/inventory/base' },
{ label: 'Draft Orders', icon: FileText, path: '/purchases/orders' }
].map(l => (
<button
key={l.label}
onClick={() => navigate(l.path)}
className="w-full flex items-center justify-between p-3.5 bg-[#f8fafc] border border-[#f1f5f9] rounded-xl hover:bg-gray-50 transition-all group"
>
<div className="flex items-center gap-3">
<div className="p-2 bg-white rounded-lg shadow-sm text-[#94a3b8] group-hover:text-[#003317] transition-colors">
<l.icon size={16} />
</div>
<span className="text-xs font-bold text-[#64748b]">{l.label}</span>
</div>
<ChevronRight size={14} className="text-[#cbd5e1] group-hover:text-[#003317] transition-all" />
</button>
))}
</div>
</div>
</div>
</div>
</div>
</div>
);
);
};
export default IntentDashboard;

View File

@@ -127,130 +127,130 @@ const IntentList: React.FC<IntentListProps> = ({ title }) => {
{/* Header Bar */}
<div className="flex items-center justify-between mb-8 bg-white p-4 rounded-3xl border border-[#e2e8f0] shadow-sm">
<div className="flex items-center gap-6">
<button className="p-2.5 bg-gray-50 rounded-xl text-[#0f4475] hover:bg-[#0f4475] hover:text-white transition-all shadow-sm">
<ArrowLeft size={18} />
</button>
<h1 className="text-xl font-black text-[#0f4475] uppercase tracking-wider">{title}</h1>
<button className="p-2.5 bg-gray-50 rounded-xl text-[#003317] hover:bg-[#003317] hover:text-white transition-all shadow-sm">
<ArrowLeft size={18} />
</button>
<h1 className="text-xl font-black text-[#003317] uppercase tracking-wider">{title}</h1>
</div>
<div className="flex items-center gap-4">
{title === 'RECEIVES' && (
<button className="bg-[#0f4475] text-white px-5 py-2.5 rounded-2xl text-xs font-black shadow-lg shadow-[#0f4475]/20 hover:bg-[#1e4e8c] transition-all">
Summary
</button>
)}
<div className="relative">
<select className="bg-gray-50 border border-[#e2e8f0] rounded-2xl px-5 py-2.5 text-xs font-bold text-[#64748b] outline-none appearance-none min-w-[150px] cursor-pointer focus:bg-white focus:ring-2 focus:ring-[#0f4475]/10">
<option>Status</option>
<option>Approved</option>
<option>Pending</option>
</select>
</div>
<button
onClick={() => setShowAddModal(true)}
className="p-2.5 bg-[#0f4475] text-white rounded-xl shadow-lg shadow-[#0f4475]/20 hover:scale-105 transition-all"
>
<Plus size={18} />
</button>
{title === 'RECEIVES' && (
<button className="bg-[#003317] text-white px-5 py-2.5 rounded-2xl text-xs font-black shadow-lg shadow-[#003317]/20 hover:bg-[#1e4e8c] transition-all">
Summary
</button>
)}
<div className="relative">
<select className="bg-gray-50 border border-[#e2e8f0] rounded-2xl px-5 py-2.5 text-xs font-bold text-[#64748b] outline-none appearance-none min-w-[150px] cursor-pointer focus:bg-white focus:ring-2 focus:ring-[#003317]/10">
<option>Status</option>
<option>Approved</option>
<option>Pending</option>
</select>
</div>
<button
onClick={() => setShowAddModal(true)}
className="p-2.5 bg-[#003317] text-white rounded-xl shadow-lg shadow-[#003317]/20 hover:scale-105 transition-all"
>
<Plus size={18} />
</button>
</div>
</div>
{/* Table Section */}
<div className="bg-white rounded-[2rem] border border-[#e2e8f0] shadow-xl shadow-indigo-500/5 overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="bg-gray-50/50">
<th className="px-8 py-5 text-left text-[11px] font-black text-[#94a3b8] uppercase tracking-[0.15em]">Purchase ID</th>
<th className="px-8 py-5 text-left text-[11px] font-black text-[#94a3b8] uppercase tracking-[0.15em]">Date</th>
<th className="px-8 py-5 text-left text-[11px] font-black text-[#94a3b8] uppercase tracking-[0.15em]">Status</th>
<th className="px-8 py-5 text-left text-[11px] font-black text-[#94a3b8] uppercase tracking-[0.15em]">Amount ()</th>
<th className="px-8 py-5 text-left text-[11px] font-black text-[#94a3b8] uppercase tracking-[0.15em]">Store name</th>
<th className="px-8 py-5 text-left text-[11px] font-black text-[#94a3b8] uppercase tracking-[0.15em]">Request status</th>
<th className="px-8 py-5 text-left text-[11px] font-black text-[#94a3b8] uppercase tracking-[0.15em]"></th>
</tr>
</thead>
<tbody className="divide-y divide-[#f1f5f9] relative">
{loading ? (
<tr>
<td colSpan={7} className="py-20 text-center">
<RefreshCw className="animate-spin text-[#0f4475] mx-auto mb-2" size={32} />
<p className="text-sm font-bold text-[#64748b]">Fetching orders...</p>
</td>
</tr>
) : data.length === 0 ? (
<tr>
<td colSpan={7} className="py-20 text-center text-[#94a3b8]">
<ShoppingBag className="mx-auto mb-4 opacity-10" size={64} />
<p className="text-sm font-bold uppercase tracking-widest">No orders found</p>
</td>
</tr>
) : (
data.map((row, i) => (
<motion.tr
key={i}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: i * 0.05 }}
className="hover:bg-gray-50/70 transition-colors group cursor-pointer"
>
<td className="px-8 py-6">
<div className="flex items-center gap-2">
<span className="text-xs font-black text-[#1e293b]">{row.purchaseId || row.id}</span>
<ChevronsRight size={12} className="text-[#cbd5e1] group-hover:text-[#0f4475] transition-colors" />
</div>
</td>
<td className="px-8 py-6 text-xs font-semibold text-[#64748b]">
{row.date ? new Date(row.date).toLocaleString('en-IN', { day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' }) : 'N/A'}
</td>
<td className="px-8 py-6">
<span className={`text-[10px] font-black px-2.5 py-1 rounded-md uppercase tracking-wider ${row.status === 'CLOSE' || row.status === 'CLOSED' ? 'text-[#ef4444] bg-[#fee2e2]' : 'text-blue-600 bg-blue-50'}`}>
{row.status}
</span>
</td>
<td className="px-8 py-6 text-xs font-black text-[#1e293b]">{row.amount?.toLocaleString()}</td>
<td className="px-8 py-6 text-xs font-bold text-[#64748b]">{row.storeName || 'Main Store'}</td>
<td className="px-8 py-6">
<span className="text-[10px] font-black text-[#10b981] bg-[#dcfce7] px-2.5 py-1 rounded-md uppercase tracking-wider">APPROVED</span>
</td>
<td className="px-8 py-6">
<div className="flex items-center gap-3">
<button className="p-2 text-[#94a3b8] hover:bg-[#0f4475] hover:text-white rounded-lg transition-all">
<Eye size={16} />
</button>
<button className="p-2 text-[#94a3b8] hover:bg-gray-100 rounded-lg transition-all">
<ChevronsRight size={16} />
</button>
</div>
</td>
</motion.tr>
))
)}
</tbody>
</table>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="bg-gray-50/50">
<th className="px-8 py-5 text-left text-[11px] font-black text-[#94a3b8] uppercase tracking-[0.15em]">Purchase ID</th>
<th className="px-8 py-5 text-left text-[11px] font-black text-[#94a3b8] uppercase tracking-[0.15em]">Date</th>
<th className="px-8 py-5 text-left text-[11px] font-black text-[#94a3b8] uppercase tracking-[0.15em]">Status</th>
<th className="px-8 py-5 text-left text-[11px] font-black text-[#94a3b8] uppercase tracking-[0.15em]">Amount ()</th>
<th className="px-8 py-5 text-left text-[11px] font-black text-[#94a3b8] uppercase tracking-[0.15em]">Store name</th>
<th className="px-8 py-5 text-left text-[11px] font-black text-[#94a3b8] uppercase tracking-[0.15em]">Request status</th>
<th className="px-8 py-5 text-left text-[11px] font-black text-[#94a3b8] uppercase tracking-[0.15em]"></th>
</tr>
</thead>
<tbody className="divide-y divide-[#f1f5f9] relative">
{loading ? (
<tr>
<td colSpan={7} className="py-20 text-center">
<RefreshCw className="animate-spin text-[#003317] mx-auto mb-2" size={32} />
<p className="text-sm font-bold text-[#64748b]">Fetching orders...</p>
</td>
</tr>
) : data.length === 0 ? (
<tr>
<td colSpan={7} className="py-20 text-center text-[#94a3b8]">
<ShoppingBag className="mx-auto mb-4 opacity-10" size={64} />
<p className="text-sm font-bold uppercase tracking-widest">No orders found</p>
</td>
</tr>
) : (
data.map((row, i) => (
<motion.tr
key={i}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: i * 0.05 }}
className="hover:bg-gray-50/70 transition-colors group cursor-pointer"
>
<td className="px-8 py-6">
<div className="flex items-center gap-2">
<span className="text-xs font-black text-[#1e293b]">{row.purchaseId || row.id}</span>
<ChevronsRight size={12} className="text-[#cbd5e1] group-hover:text-[#003317] transition-colors" />
</div>
</td>
<td className="px-8 py-6 text-xs font-semibold text-[#64748b]">
{row.date ? new Date(row.date).toLocaleString('en-IN', { day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' }) : 'N/A'}
</td>
<td className="px-8 py-6">
<span className={`text-[10px] font-black px-2.5 py-1 rounded-md uppercase tracking-wider ${row.status === 'CLOSE' || row.status === 'CLOSED' ? 'text-[#ef4444] bg-[#fee2e2]' : 'text-blue-600 bg-blue-50'}`}>
{row.status}
</span>
</td>
<td className="px-8 py-6 text-xs font-black text-[#1e293b]">{row.amount?.toLocaleString()}</td>
<td className="px-8 py-6 text-xs font-bold text-[#64748b]">{row.storeName || 'Main Store'}</td>
<td className="px-8 py-6">
<span className="text-[10px] font-black text-[#10b981] bg-[#dcfce7] px-2.5 py-1 rounded-md uppercase tracking-wider">APPROVED</span>
</td>
<td className="px-8 py-6">
<div className="flex items-center gap-3">
<button className="p-2 text-[#94a3b8] hover:bg-[#003317] hover:text-white rounded-lg transition-all">
<Eye size={16} />
</button>
<button className="p-2 text-[#94a3b8] hover:bg-gray-100 rounded-lg transition-all">
<ChevronsRight size={16} />
</button>
</div>
</td>
</motion.tr>
))
)}
</tbody>
</table>
</div>
{/* Pagination Bar */}
<div className="bg-gray-50/50 px-8 py-4 border-t border-[#f1f5f9] flex items-center justify-end gap-8">
<div className="flex items-center gap-3 text-xs font-bold text-[#64748b]">
<span>Rows per page:</span>
<select className="bg-transparent border-0 font-black text-[#1e293b] outline-none cursor-pointer">
<option>10</option>
<option>20</option>
</select>
</div>
<div className="text-xs font-bold text-[#64748b]">
1-5 of 5
</div>
<div className="flex items-center gap-2">
<button className="p-1 text-[#cbd5e1] cursor-not-allowed"><ChevronLeft size={18} /></button>
<button className="p-1 text-[#cbd5e1] cursor-not-allowed"><ChevronRight size={18} /></button>
</div>
</div>
{/* Pagination Bar */}
<div className="bg-gray-50/50 px-8 py-4 border-t border-[#f1f5f9] flex items-center justify-end gap-8">
<div className="flex items-center gap-3 text-xs font-bold text-[#64748b]">
<span>Rows per page:</span>
<select className="bg-transparent border-0 font-black text-[#1e293b] outline-none cursor-pointer">
<option>10</option>
<option>20</option>
</select>
</div>
<div className="text-xs font-bold text-[#64748b]">
1-5 of 5
</div>
<div className="flex items-center gap-2">
<button className="p-1 text-[#cbd5e1] cursor-not-allowed"><ChevronLeft size={18} /></button>
<button className="p-1 text-[#cbd5e1] cursor-not-allowed"><ChevronRight size={18} /></button>
</div>
</div>
</div>
{/* Modern Add Order Modal */}
{showAddModal && (
<div className="fixed inset-0 bg-[#0f4475]/30 backdrop-blur-md z-[100] flex items-center justify-center p-4">
<div className="fixed inset-0 bg-[#003317]/30 backdrop-blur-md z-[100] flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
@@ -259,7 +259,7 @@ const IntentList: React.FC<IntentListProps> = ({ title }) => {
{/* Modal Header */}
<div className="px-10 py-8 border-b border-[#f1f5f9] flex justify-between items-center bg-gray-50/30">
<div className="flex items-center gap-5">
<div className="w-14 h-14 bg-gradient-to-br from-[#0f4475] to-[#1e4e8c] rounded-2xl flex items-center justify-center text-white shadow-xl shadow-[#0f4475]/20">
<div className="w-14 h-14 bg-gradient-to-br from-[#003317] to-[#1e4e8c] rounded-2xl flex items-center justify-center text-white shadow-xl shadow-[#003317]/20">
<ShoppingCart size={24} />
</div>
<div>
@@ -280,19 +280,19 @@ const IntentList: React.FC<IntentListProps> = ({ title }) => {
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
<div className="space-y-2">
<label className="text-[10px] font-black text-[#94a3b8] uppercase tracking-widest ml-1">Order ID</label>
<input type="text" value={newOrder.purchaseId} readOnly className="w-full px-5 py-3.5 bg-gray-50 border border-[#e2e8f0] rounded-2xl text-sm font-black text-[#0f4475] outline-none" />
<input type="text" value={newOrder.purchaseId} readOnly className="w-full px-5 py-3.5 bg-gray-50 border border-[#e2e8f0] rounded-2xl text-sm font-black text-[#003317] outline-none" />
</div>
<div className="space-y-2">
<label className="text-[10px] font-black text-[#94a3b8] uppercase tracking-widest ml-1">Date</label>
<input type="date" value={newOrder.date} onChange={e => setNewOrder({...newOrder, date: e.target.value})} className="w-full px-5 py-3.5 bg-white border border-[#e2e8f0] rounded-2xl text-sm font-bold text-[#1e293b] outline-none focus:ring-4 focus:ring-[#0f4475]/5 focus:border-[#0f4475] transition-all" />
<input type="date" value={newOrder.date} onChange={e => setNewOrder({ ...newOrder, date: e.target.value })} className="w-full px-5 py-3.5 bg-white border border-[#e2e8f0] rounded-2xl text-sm font-bold text-[#1e293b] outline-none focus:ring-4 focus:ring-[#003317]/5 focus:border-[#003317] transition-all" />
</div>
<div className="space-y-2">
<label className="text-[10px] font-black text-[#94a3b8] uppercase tracking-widest ml-1">Select Vendor</label>
<select
required
value={newOrder.vendorId}
onChange={e => setNewOrder({...newOrder, vendorId: e.target.value})}
className="w-full px-5 py-3.5 bg-white border border-[#e2e8f0] rounded-2xl text-sm font-bold text-[#1e293b] outline-none focus:ring-4 focus:ring-[#0f4475]/5 focus:border-[#0f4475] transition-all appearance-none cursor-pointer"
onChange={e => setNewOrder({ ...newOrder, vendorId: e.target.value })}
className="w-full px-5 py-3.5 bg-white border border-[#e2e8f0] rounded-2xl text-sm font-bold text-[#1e293b] outline-none focus:ring-4 focus:ring-[#003317]/5 focus:border-[#003317] transition-all appearance-none cursor-pointer"
>
<option value="">Choose Supplier</option>
{vendors.map(v => <option key={v.id} value={v.id}>{v.name}</option>)}
@@ -301,8 +301,8 @@ const IntentList: React.FC<IntentListProps> = ({ title }) => {
<div className="space-y-2">
<label className="text-[10px] font-black text-[#94a3b8] uppercase tracking-widest ml-1">Status</label>
<div className="px-5 py-3.5 bg-[#dcfce7] border border-[#10b981]/20 rounded-2xl text-[11px] font-black text-[#10b981] uppercase tracking-widest flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-[#10b981] animate-pulse" />
{newOrder.status}
<span className="w-2 h-2 rounded-full bg-[#10b981] animate-pulse" />
{newOrder.status}
</div>
</div>
</div>
@@ -310,88 +310,88 @@ const IntentList: React.FC<IntentListProps> = ({ title }) => {
{/* Items Table */}
<div className="space-y-4">
<div className="flex items-center justify-between px-2">
<h3 className="text-xs font-black text-[#1e293b] uppercase tracking-[0.2em]">Order Line Items</h3>
<button type="button" onClick={handleAddItem} className="flex items-center gap-2 px-4 py-2 bg-[#0f4475]/5 text-[#0f4475] rounded-xl text-[11px] font-black hover:bg-[#0f4475] hover:text-white transition-all">
<Plus size={14} /> Add Product
</button>
<h3 className="text-xs font-black text-[#1e293b] uppercase tracking-[0.2em]">Order Line Items</h3>
<button type="button" onClick={handleAddItem} className="flex items-center gap-2 px-4 py-2 bg-[#003317]/5 text-[#003317] rounded-xl text-[11px] font-black hover:bg-[#003317] hover:text-white transition-all">
<Plus size={14} /> Add Product
</button>
</div>
<div className="border border-[#e2e8f0] rounded-[2rem] overflow-hidden bg-gray-50/30">
<table className="w-full text-left">
<thead className="bg-white border-b border-[#e2e8f0]">
<tr>
<th className="px-6 py-4 text-[10px] font-black text-[#94a3b8] uppercase tracking-widest">Product / Material Name</th>
<th className="px-6 py-4 text-[10px] font-black text-[#94a3b8] uppercase tracking-widest w-32 text-center">Qty</th>
<th className="px-6 py-4 text-[10px] font-black text-[#94a3b8] uppercase tracking-widest w-40">Rate ()</th>
<th className="px-6 py-4 text-[10px] font-black text-[#94a3b8] uppercase tracking-widest w-40">Subtotal</th>
<th className="px-6 py-4 text-[10px] font-black text-[#94a3b8] uppercase tracking-widest w-16"></th>
</tr>
</thead>
<tbody className="divide-y divide-[#f1f5f9]">
{newOrder.items.map((item, idx) => (
<tr key={idx} className="group hover:bg-white transition-colors">
<td className="px-6 py-4">
<input type="text" placeholder="Start typing product name..." value={item.productName} onChange={e => handleItemChange(idx, 'productName', e.target.value)} className="w-full bg-transparent text-sm font-bold text-[#1e293b] outline-none placeholder:text-[#cbd5e1]" />
</td>
<td className="px-6 py-4">
<input type="number" min="1" value={item.quantity} onChange={e => handleItemChange(idx, 'quantity', parseFloat(e.target.value))} className="w-full bg-transparent text-sm font-black text-[#1e293b] outline-none text-center" />
</td>
<td className="px-6 py-4">
<input type="number" step="0.01" value={item.rate} onChange={e => handleItemChange(idx, 'rate', parseFloat(e.target.value))} className="w-full bg-transparent text-sm font-bold text-[#1e293b] outline-none" />
</td>
<td className="px-6 py-4">
<span className="text-sm font-black text-[#0f4475]">{item.total.toLocaleString()}</span>
</td>
<td className="px-6 py-4">
<button type="button" onClick={() => setNewOrder({...newOrder, items: newOrder.items.filter((_, i) => i !== idx)})} className="p-2 text-[#cbd5e1] hover:text-[#ef4444] transition-colors">
<Trash2 size={16} />
</button>
</td>
</tr>
))}
</tbody>
</table>
<table className="w-full text-left">
<thead className="bg-white border-b border-[#e2e8f0]">
<tr>
<th className="px-6 py-4 text-[10px] font-black text-[#94a3b8] uppercase tracking-widest">Product / Material Name</th>
<th className="px-6 py-4 text-[10px] font-black text-[#94a3b8] uppercase tracking-widest w-32 text-center">Qty</th>
<th className="px-6 py-4 text-[10px] font-black text-[#94a3b8] uppercase tracking-widest w-40">Rate ()</th>
<th className="px-6 py-4 text-[10px] font-black text-[#94a3b8] uppercase tracking-widest w-40">Subtotal</th>
<th className="px-6 py-4 text-[10px] font-black text-[#94a3b8] uppercase tracking-widest w-16"></th>
</tr>
</thead>
<tbody className="divide-y divide-[#f1f5f9]">
{newOrder.items.map((item, idx) => (
<tr key={idx} className="group hover:bg-white transition-colors">
<td className="px-6 py-4">
<input type="text" placeholder="Start typing product name..." value={item.productName} onChange={e => handleItemChange(idx, 'productName', e.target.value)} className="w-full bg-transparent text-sm font-bold text-[#1e293b] outline-none placeholder:text-[#cbd5e1]" />
</td>
<td className="px-6 py-4">
<input type="number" min="1" value={item.quantity} onChange={e => handleItemChange(idx, 'quantity', parseFloat(e.target.value))} className="w-full bg-transparent text-sm font-black text-[#1e293b] outline-none text-center" />
</td>
<td className="px-6 py-4">
<input type="number" step="0.01" value={item.rate} onChange={e => handleItemChange(idx, 'rate', parseFloat(e.target.value))} className="w-full bg-transparent text-sm font-bold text-[#1e293b] outline-none" />
</td>
<td className="px-6 py-4">
<span className="text-sm font-black text-[#003317]">{item.total.toLocaleString()}</span>
</td>
<td className="px-6 py-4">
<button type="button" onClick={() => setNewOrder({ ...newOrder, items: newOrder.items.filter((_, i) => i !== idx) })} className="p-2 text-[#cbd5e1] hover:text-[#ef4444] transition-colors">
<Trash2 size={16} />
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Bottom Section: Notes & Totals */}
<div className="grid grid-cols-1 md:grid-cols-12 gap-10 pt-4">
<div className="md:col-span-7 space-y-6">
<div className="space-y-2">
<label className="text-[10px] font-black text-[#94a3b8] uppercase tracking-widest ml-1">Reference ID / PO Number</label>
<input type="text" value={newOrder.referenceId} onChange={e => setNewOrder({...newOrder, referenceId: e.target.value})} className="w-full px-5 py-4 bg-white border border-[#e2e8f0] rounded-2xl text-sm font-bold outline-none focus:border-[#0f4475] transition-all" placeholder="Enter external reference number if any..." />
</div>
<div className="space-y-2">
<label className="text-[10px] font-black text-[#94a3b8] uppercase tracking-widest ml-1">Special Instructions</label>
<textarea rows={3} value={newOrder.instruction} onChange={e => setNewOrder({...newOrder, instruction: e.target.value})} className="w-full px-5 py-4 bg-white border border-[#e2e8f0] rounded-2xl text-sm font-bold outline-none focus:border-[#0f4475] transition-all resize-none" placeholder="Add any specific delivery or quality requirements..." />
</div>
<div className="space-y-2">
<label className="text-[10px] font-black text-[#94a3b8] uppercase tracking-widest ml-1">Reference ID / PO Number</label>
<input type="text" value={newOrder.referenceId} onChange={e => setNewOrder({ ...newOrder, referenceId: e.target.value })} className="w-full px-5 py-4 bg-white border border-[#e2e8f0] rounded-2xl text-sm font-bold outline-none focus:border-[#003317] transition-all" placeholder="Enter external reference number if any..." />
</div>
<div className="space-y-2">
<label className="text-[10px] font-black text-[#94a3b8] uppercase tracking-widest ml-1">Special Instructions</label>
<textarea rows={3} value={newOrder.instruction} onChange={e => setNewOrder({ ...newOrder, instruction: e.target.value })} className="w-full px-5 py-4 bg-white border border-[#e2e8f0] rounded-2xl text-sm font-bold outline-none focus:border-[#003317] transition-all resize-none" placeholder="Add any specific delivery or quality requirements..." />
</div>
</div>
<div className="md:col-span-5 flex flex-col justify-end">
<div className="bg-[#1e293b] rounded-[2.5rem] p-8 text-white space-y-5 shadow-2xl shadow-[#1e293b]/20">
<div className="flex justify-between items-center text-xs opacity-60 font-bold uppercase tracking-wider">
<span>Order Subtotal</span>
<span>{newOrder.items.reduce((s, i) => s + i.total, 0).toLocaleString()}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-xs opacity-60 font-bold uppercase tracking-wider">Adjustment / Discount</span>
<div className="flex items-center gap-2">
<span className="text-[#ef4444] font-black">-</span>
<input type="number" value={newOrder.discount} onChange={e => setNewOrder({...newOrder, discount: parseFloat(e.target.value) || 0})} className="w-24 text-right bg-white/10 border border-white/10 rounded-xl px-3 py-1.5 text-sm font-black text-white outline-none focus:bg-white/20 transition-all" />
</div>
</div>
<div className="h-[1px] bg-white/10 my-2"></div>
<div className="flex justify-between items-end">
<div>
<p className="text-[10px] opacity-40 font-black uppercase tracking-[0.2em] mb-1">Grand Payable</p>
<p className="text-3xl font-black tracking-tighter">{calculateTotal().toLocaleString()}</p>
</div>
<div className="text-right">
<p className="text-[10px] opacity-40 font-black uppercase tracking-[0.2em] mb-1">Total Items</p>
<p className="text-xl font-black">{newOrder.items.length}</p>
</div>
</div>
<div className="bg-[#1e293b] rounded-[2.5rem] p-8 text-white space-y-5 shadow-2xl shadow-[#1e293b]/20">
<div className="flex justify-between items-center text-xs opacity-60 font-bold uppercase tracking-wider">
<span>Order Subtotal</span>
<span>{newOrder.items.reduce((s, i) => s + i.total, 0).toLocaleString()}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-xs opacity-60 font-bold uppercase tracking-wider">Adjustment / Discount</span>
<div className="flex items-center gap-2">
<span className="text-[#ef4444] font-black">-</span>
<input type="number" value={newOrder.discount} onChange={e => setNewOrder({ ...newOrder, discount: parseFloat(e.target.value) || 0 })} className="w-24 text-right bg-white/10 border border-white/10 rounded-xl px-3 py-1.5 text-sm font-black text-white outline-none focus:bg-white/20 transition-all" />
</div>
</div>
<div className="h-[1px] bg-white/10 my-2"></div>
<div className="flex justify-between items-end">
<div>
<p className="text-[10px] opacity-40 font-black uppercase tracking-[0.2em] mb-1">Grand Payable</p>
<p className="text-3xl font-black tracking-tighter">{calculateTotal().toLocaleString()}</p>
</div>
<div className="text-right">
<p className="text-[10px] opacity-40 font-black uppercase tracking-[0.2em] mb-1">Total Items</p>
<p className="text-xl font-black">{newOrder.items.length}</p>
</div>
</div>
</div>
</div>
</div>
@@ -407,14 +407,14 @@ const IntentList: React.FC<IntentListProps> = ({ title }) => {
<button
type="submit"
disabled={isSaving}
className="flex-1 px-10 py-5 bg-gradient-to-r from-[#0f4475] to-[#1e4e8c] text-white font-black rounded-2xl shadow-2xl shadow-[#0f4475]/30 hover:scale-[1.02] active:scale-[0.98] transition-all flex items-center justify-center gap-4 uppercase tracking-[0.2em] text-[11px]"
className="flex-1 px-10 py-5 bg-gradient-to-r from-[#003317] to-[#1e4e8c] text-white font-black rounded-2xl shadow-2xl shadow-[#003317]/30 hover:scale-[1.02] active:scale-[0.98] transition-all flex items-center justify-center gap-4 uppercase tracking-[0.2em] text-[11px]"
>
{isSaving ? <Loader2 className="animate-spin" size={18} /> : (
<>
<CheckCircle size={18} />
Finalize & Confirm Order
</>
)}
{isSaving ? <Loader2 className="animate-spin" size={18} /> : (
<>
<CheckCircle size={18} />
Finalize & Confirm Order
</>
)}
</button>
</div>
</form>

View File

@@ -1,378 +1,378 @@
import { apiFetch } from '../api';
import { useState, useEffect } from 'react';
import {
Ticket,
Plus,
Trash2,
Power,
Calendar,
Users,
Clock,
ChevronRight,
TrendingUp,
Tag,
AlertCircle,
X,
Target
Ticket,
Plus,
Trash2,
Power,
Calendar,
Users,
Clock,
ChevronRight,
TrendingUp,
Tag,
AlertCircle,
X,
Target
} from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { format } from 'date-fns';
interface Coupon {
id: number;
code: string;
rewardAmount: number;
expiryDate: string;
maxClaims: number;
currentClaims: number;
isActive: boolean;
description: string;
createdAt: string;
id: number;
code: string;
rewardAmount: number;
expiryDate: string;
maxClaims: number;
currentClaims: number;
isActive: boolean;
description: string;
createdAt: string;
}
const ManageCoupons = () => {
const [coupons, setCoupons] = useState<Coupon[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isModalOpen, setIsModalOpen] = useState(false);
const [newCoupon, setNewCoupon] = useState({
code: '',
rewardAmount: '',
expiryDate: '',
maxClaims: '',
description: ''
});
const [coupons, setCoupons] = useState<Coupon[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isModalOpen, setIsModalOpen] = useState(false);
const [newCoupon, setNewCoupon] = useState({
code: '',
rewardAmount: '',
expiryDate: '',
maxClaims: '',
description: ''
});
const fetchCoupons = async () => {
try {
setIsLoading(true);
const response = await apiFetch('/api/coupons');
if (response.ok) {
const data = await response.json();
setCoupons(data);
const fetchCoupons = async () => {
try {
setIsLoading(true);
const response = await apiFetch('/api/coupons');
if (response.ok) {
const data = await response.json();
setCoupons(data);
}
} catch (error) {
console.error('Error fetching coupons:', error);
} finally {
setIsLoading(false);
}
} catch (error) {
console.error('Error fetching coupons:', error);
} finally {
setIsLoading(false);
}
};
};
useEffect(() => {
fetchCoupons();
}, []);
useEffect(() => {
fetchCoupons();
}, []);
const handleCreateCoupon = async (e: React.FormEvent) => {
e.preventDefault();
try {
const response = await apiFetch('/api/coupons', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...newCoupon,
rewardAmount: Number(newCoupon.rewardAmount),
maxClaims: Number(newCoupon.maxClaims),
expiryDate: new Date(newCoupon.expiryDate).toISOString()
})
});
if (response.ok) {
setIsModalOpen(false);
setNewCoupon({ code: '', rewardAmount: '', expiryDate: '', maxClaims: '', description: '' });
fetchCoupons();
const handleCreateCoupon = async (e: React.FormEvent) => {
e.preventDefault();
try {
const response = await apiFetch('/api/coupons', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...newCoupon,
rewardAmount: Number(newCoupon.rewardAmount),
maxClaims: Number(newCoupon.maxClaims),
expiryDate: new Date(newCoupon.expiryDate).toISOString()
})
});
if (response.ok) {
setIsModalOpen(false);
setNewCoupon({ code: '', rewardAmount: '', expiryDate: '', maxClaims: '', description: '' });
fetchCoupons();
}
} catch (error) {
console.error('Error creating coupon:', error);
}
} catch (error) {
console.error('Error creating coupon:', error);
}
};
};
const handleToggleStatus = async (id: number) => {
try {
const response = await apiFetch(`/api/coupons/${id}/toggle`, { method: 'PATCH' });
if (response.ok) fetchCoupons();
} catch (error) {
console.error('Error toggling status:', error);
}
};
const handleToggleStatus = async (id: number) => {
try {
const response = await apiFetch(`/api/coupons/${id}/toggle`, { method: 'PATCH' });
if (response.ok) fetchCoupons();
} catch (error) {
console.error('Error toggling status:', error);
}
};
const handleDelete = async (id: number) => {
if (!window.confirm('Are you sure you want to delete this coupon?')) return;
try {
const response = await apiFetch(`/api/coupons/${id}`, { method: 'DELETE' });
if (response.ok) fetchCoupons();
} catch (error) {
console.error('Error deleting coupon:', error);
}
};
const handleDelete = async (id: number) => {
if (!window.confirm('Are you sure you want to delete this coupon?')) return;
try {
const response = await apiFetch(`/api/coupons/${id}`, { method: 'DELETE' });
if (response.ok) fetchCoupons();
} catch (error) {
console.error('Error deleting coupon:', error);
}
};
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-[#0f4475] font-black uppercase tracking-widest text-sm flex flex-col items-center gap-4"
>
<Ticket size={32} className="animate-pulse" />
Synchronizing Coupon Network...
</motion.div>
</div>
);
}
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 flex flex-col items-center gap-4"
>
<Ticket size={32} className="animate-pulse" />
Synchronizing Coupon Network...
</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">
<h2 className="text-xs font-black text-[#0f4475] uppercase tracking-widest">Ritz Rewards</h2>
</div>
<h1 className="text-2xl font-black text-slate-800">Coupon Codes</h1>
<p className="text-[10px] text-slate-400 font-medium flex items-center gap-1.5 uppercase tracking-wider">
Manage redemption codes and promotional credits for Ritz tokens
</p>
</div>
<button
onClick={() => setIsModalOpen(true)}
className="flex items-center gap-2 bg-[#0f4475] text-white px-6 py-2.5 rounded-xl text-xs font-black uppercase tracking-widest hover:scale-105 transition-all shadow-lg shadow-[#0f4475]/20 active:scale-95"
>
<Plus size={18} />
Generate New Code
</button>
</div>
{/* Analytics Summary */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="bg-white p-6 rounded-[2rem] border border-slate-100 shadow-sm group hover:border-[#0f4475]/30 transition-all">
<div className="flex justify-between items-start mb-4">
<div className="p-3 bg-indigo-50 text-[#0f4475] rounded-xl">
<Tag size={20} />
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">
<h2 className="text-xs font-black text-[#003317] uppercase tracking-widest">Ritz Rewards</h2>
</div>
<span className="text-[10px] font-black text-indigo-400 uppercase tracking-widest">Active Pool</span>
<h1 className="text-2xl font-black text-slate-800">Coupon Codes</h1>
<p className="text-[10px] text-slate-400 font-medium flex items-center gap-1.5 uppercase tracking-wider">
Manage redemption codes and promotional credits for Ritz tokens
</p>
</div>
<h3 className="text-slate-400 text-xs font-black uppercase tracking-wider">Active Coupons</h3>
<p className="text-3xl font-black text-slate-800 mt-1">{coupons.filter(c => c.isActive).length}</p>
<button
onClick={() => setIsModalOpen(true)}
className="flex items-center gap-2 bg-[#003317] text-white px-6 py-2.5 rounded-xl text-xs font-black uppercase tracking-widest hover:scale-105 transition-all shadow-lg shadow-[#003317]/20 active:scale-95"
>
<Plus size={18} />
Generate New Code
</button>
</div>
<div className="bg-white p-6 rounded-[2rem] border border-slate-100 shadow-sm group hover:border-emerald-500/30 transition-all">
<div className="flex justify-between items-start mb-4">
<div className="p-3 bg-emerald-50 text-emerald-600 rounded-xl">
<TrendingUp size={20} />
</div>
<span className="text-[10px] font-black text-emerald-400 uppercase tracking-widest">Global Reach</span>
</div>
<h3 className="text-slate-400 text-xs font-black uppercase tracking-wider">Total Redemptions</h3>
<p className="text-3xl font-black text-slate-800 mt-1">{coupons.reduce((acc, current) => acc + current.currentClaims, 0)}</p>
</div>
<div className="bg-white p-6 rounded-[2rem] border border-slate-100 shadow-sm group hover:border-amber-500/30 transition-all">
<div className="flex justify-between items-start mb-4">
<div className="p-3 bg-amber-50 text-amber-600 rounded-xl">
<Clock size={20} />
</div>
<span className="text-[10px] font-black text-amber-400 uppercase tracking-widest">Expiring Soon</span>
</div>
<h3 className="text-slate-400 text-xs font-black uppercase tracking-wider">Limited Availability</h3>
<p className="text-3xl font-black text-slate-800 mt-1">
{coupons.filter(c => new Date(c.expiryDate).getTime() < new Date().getTime() + 86400000 * 3 && c.isActive).length}
</p>
</div>
</div>
{/* Coupons Grid */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 pb-20">
<AnimatePresence mode="popLayout">
{coupons.map((coupon) => (
<motion.div
layout
key={coupon.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95 }}
className={`bg-white rounded-[2.5rem] p-6 border-2 transition-all relative overflow-hidden group ${coupon.isActive ? 'border-slate-100' : 'border-slate-200 grayscale opacity-75 bg-slate-50'}`}
>
{/* Status Indicator */}
<div className={`absolute top-6 right-6 px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest ${coupon.isActive ? 'bg-emerald-50 text-emerald-600' : 'bg-slate-200 text-slate-500'}`}>
{coupon.isActive ? 'Operational' : 'Deactivated'}
{/* Analytics Summary */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="bg-white p-6 rounded-[2rem] border border-slate-100 shadow-sm group hover:border-[#003317]/30 transition-all">
<div className="flex justify-between items-start mb-4">
<div className="p-3 bg-indigo-50 text-[#003317] rounded-xl">
<Tag size={20} />
</div>
<span className="text-[10px] font-black text-indigo-400 uppercase tracking-widest">Active Pool</span>
</div>
<h3 className="text-slate-400 text-xs font-black uppercase tracking-wider">Active Coupons</h3>
<p className="text-3xl font-black text-slate-800 mt-1">{coupons.filter(c => c.isActive).length}</p>
</div>
<div className="flex gap-6">
<div className={`w-24 h-24 rounded-3xl flex flex-col items-center justify-center border-2 border-dashed ${coupon.isActive ? 'bg-indigo-50/50 border-[#0f4475]/20' : 'bg-slate-100 border-slate-300'}`}>
<Ticket size={24} className={coupon.isActive ? 'text-[#0f4475]' : 'text-slate-400'} />
<span className={`text-[10px] font-black mt-2 uppercase tracking-tight ${coupon.isActive ? 'text-[#0f4475]' : 'text-slate-500'}`}>Redeem</span>
<div className="bg-white p-6 rounded-[2rem] border border-slate-100 shadow-sm group hover:border-emerald-500/30 transition-all">
<div className="flex justify-between items-start mb-4">
<div className="p-3 bg-emerald-50 text-emerald-600 rounded-xl">
<TrendingUp size={20} />
</div>
<span className="text-[10px] font-black text-emerald-400 uppercase tracking-widest">Global Reach</span>
</div>
<h3 className="text-slate-400 text-xs font-black uppercase tracking-wider">Total Redemptions</h3>
<p className="text-3xl font-black text-slate-800 mt-1">{coupons.reduce((acc, current) => acc + current.currentClaims, 0)}</p>
</div>
<div className="bg-white p-6 rounded-[2rem] border border-slate-100 shadow-sm group hover:border-amber-500/30 transition-all">
<div className="flex justify-between items-start mb-4">
<div className="p-3 bg-amber-50 text-amber-600 rounded-xl">
<Clock size={20} />
</div>
<span className="text-[10px] font-black text-amber-400 uppercase tracking-widest">Expiring Soon</span>
</div>
<h3 className="text-slate-400 text-xs font-black uppercase tracking-wider">Limited Availability</h3>
<p className="text-3xl font-black text-slate-800 mt-1">
{coupons.filter(c => new Date(c.expiryDate).getTime() < new Date().getTime() + 86400000 * 3 && c.isActive).length}
</p>
</div>
</div>
{/* Coupons Grid */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 pb-20">
<AnimatePresence mode="popLayout">
{coupons.map((coupon) => (
<motion.div
layout
key={coupon.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95 }}
className={`bg-white rounded-[2.5rem] p-6 border-2 transition-all relative overflow-hidden group ${coupon.isActive ? 'border-slate-100' : 'border-slate-200 grayscale opacity-75 bg-slate-50'}`}
>
{/* Status Indicator */}
<div className={`absolute top-6 right-6 px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest ${coupon.isActive ? 'bg-emerald-50 text-emerald-600' : 'bg-slate-200 text-slate-500'}`}>
{coupon.isActive ? 'Operational' : 'Deactivated'}
</div>
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<h3 className="text-2xl font-black text-slate-800 tracking-tighter uppercase">{coupon.code}</h3>
{!coupon.isActive && <AlertCircle size={14} className="text-slate-400" />}
<div className="flex gap-6">
<div className={`w-24 h-24 rounded-3xl flex flex-col items-center justify-center border-2 border-dashed ${coupon.isActive ? 'bg-indigo-50/50 border-[#003317]/20' : 'bg-slate-100 border-slate-300'}`}>
<Ticket size={24} className={coupon.isActive ? 'text-[#003317]' : 'text-slate-400'} />
<span className={`text-[10px] font-black mt-2 uppercase tracking-tight ${coupon.isActive ? 'text-[#003317]' : 'text-slate-500'}`}>Redeem</span>
</div>
<p className="text-xs text-slate-400 font-bold uppercase tracking-widest mb-4">{coupon.description || 'Global Promotional Credit'}</p>
<div className="grid grid-cols-2 gap-4">
<div className="flex items-center gap-2.5">
<div className="p-1.5 bg-slate-50 rounded-lg"><Target size={14} className="text-slate-400" /></div>
<div>
<p className="text-[10px] font-black text-slate-400 uppercase leading-none mb-0.5">Reward</p>
<p className="text-sm font-black text-slate-700 leading-none">R{coupon.rewardAmount}</p>
</div>
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<h3 className="text-2xl font-black text-slate-800 tracking-tighter uppercase">{coupon.code}</h3>
{!coupon.isActive && <AlertCircle size={14} className="text-slate-400" />}
</div>
<div className="flex items-center gap-2.5">
<div className="p-1.5 bg-slate-50 rounded-lg"><Calendar size={14} className="text-slate-400" /></div>
<div>
<p className="text-[10px] font-black text-slate-400 uppercase leading-none mb-0.5">Expires</p>
<p className="text-sm font-black text-slate-700 leading-none">{format(new Date(coupon.expiryDate), 'dd MMM yyyy')}</p>
<p className="text-xs text-slate-400 font-bold uppercase tracking-widest mb-4">{coupon.description || 'Global Promotional Credit'}</p>
<div className="grid grid-cols-2 gap-4">
<div className="flex items-center gap-2.5">
<div className="p-1.5 bg-slate-50 rounded-lg"><Target size={14} className="text-slate-400" /></div>
<div>
<p className="text-[10px] font-black text-slate-400 uppercase leading-none mb-0.5">Reward</p>
<p className="text-sm font-black text-slate-700 leading-none">R{coupon.rewardAmount}</p>
</div>
</div>
<div className="flex items-center gap-2.5">
<div className="p-1.5 bg-slate-50 rounded-lg"><Calendar size={14} className="text-slate-400" /></div>
<div>
<p className="text-[10px] font-black text-slate-400 uppercase leading-none mb-0.5">Expires</p>
<p className="text-sm font-black text-slate-700 leading-none">{format(new Date(coupon.expiryDate), 'dd MMM yyyy')}</p>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Usage Meter */}
<div className="mt-8 pt-6 border-t border-slate-50">
<div className="flex justify-between items-center mb-2">
<div className="flex items-center gap-1.5">
<Users size={14} className="text-slate-400" />
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Usage Dynamics</span>
{/* Usage Meter */}
<div className="mt-8 pt-6 border-t border-slate-50">
<div className="flex justify-between items-center mb-2">
<div className="flex items-center gap-1.5">
<Users size={14} className="text-slate-400" />
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Usage Dynamics</span>
</div>
<span className="text-[10px] font-black text-slate-800 uppercase tracking-widest">{coupon.currentClaims} / {coupon.maxClaims} Clm.</span>
</div>
<div className="w-full h-2 bg-slate-100 rounded-full overflow-hidden">
<motion.div
initial={{ width: 0 }}
animate={{ width: `${(coupon.currentClaims / coupon.maxClaims) * 100}%` }}
className={`h-full rounded-full ${coupon.isActive ? 'bg-[#003317]' : 'bg-slate-400'}`}
/>
</div>
<span className="text-[10px] font-black text-slate-800 uppercase tracking-widest">{coupon.currentClaims} / {coupon.maxClaims} Clm.</span>
</div>
<div className="w-full h-2 bg-slate-100 rounded-full overflow-hidden">
<motion.div
initial={{ width: 0 }}
animate={{ width: `${(coupon.currentClaims / coupon.maxClaims) * 100}%` }}
className={`h-full rounded-full ${coupon.isActive ? 'bg-[#0f4475]' : 'bg-slate-400'}`}
/>
</div>
</div>
{/* Actions */}
<div className="absolute bottom-6 right-6 flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
<button
onClick={() => handleToggleStatus(coupon.id)}
title={coupon.isActive ? 'Deactivate Coupon' : 'Activate Coupon'}
className={`p-2 rounded-xl transition-all ${coupon.isActive ? 'bg-amber-50 text-amber-600 hover:bg-amber-600 hover:text-white' : 'bg-emerald-50 text-emerald-600 hover:bg-emerald-600 hover:text-white'}`}
>
<Power size={16} />
</button>
<button
onClick={() => handleDelete(coupon.id)}
title="Permanently Delete"
className="p-2 bg-rose-50 text-rose-500 rounded-xl hover:bg-rose-500 hover:text-white transition-all"
>
<Trash2 size={16} />
</button>
</div>
</motion.div>
))}
{/* Actions */}
<div className="absolute bottom-6 right-6 flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
<button
onClick={() => handleToggleStatus(coupon.id)}
title={coupon.isActive ? 'Deactivate Coupon' : 'Activate Coupon'}
className={`p-2 rounded-xl transition-all ${coupon.isActive ? 'bg-amber-50 text-amber-600 hover:bg-amber-600 hover:text-white' : 'bg-emerald-50 text-emerald-600 hover:bg-emerald-600 hover:text-white'}`}
>
<Power size={16} />
</button>
<button
onClick={() => handleDelete(coupon.id)}
title="Permanently Delete"
className="p-2 bg-rose-50 text-rose-500 rounded-xl hover:bg-rose-500 hover:text-white transition-all"
>
<Trash2 size={16} />
</button>
</div>
</motion.div>
))}
</AnimatePresence>
</div>
{/* Creation Modal */}
<AnimatePresence>
{isModalOpen && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setIsModalOpen(false)}
className="absolute inset-0 bg-[#003317]/40 backdrop-blur-md"
/>
<motion.div
initial={{ scale: 0.9, opacity: 0, y: 20 }}
animate={{ scale: 1, opacity: 1, y: 0 }}
exit={{ scale: 0.9, opacity: 0, y: 20 }}
className="bg-white w-full max-w-lg rounded-[3rem] p-8 shadow-2xl relative z-10 border border-white/20"
>
<div className="flex justify-between items-center mb-8">
<div className="flex items-center gap-3">
<div className="p-2 bg-indigo-50 text-[#003317] rounded-xl">
<Ticket size={24} />
</div>
<h2 className="text-xl font-black text-slate-800 uppercase tracking-tight">Generate Code</h2>
</div>
<button onClick={() => setIsModalOpen(false)} className="p-2 hover:bg-slate-100 rounded-full transition-colors">
<X size={20} className="text-slate-400" />
</button>
</div>
<form onSubmit={handleCreateCoupon} className="space-y-6">
<div className="grid grid-cols-2 gap-6">
<div className="space-y-1.5">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Reference Code</label>
<input
required
type="text"
placeholder="e.g. WELCOME50"
value={newCoupon.code}
onChange={(e) => setNewCoupon({ ...newCoupon, code: e.target.value.toUpperCase() })}
className="w-full bg-slate-50 border border-slate-100 rounded-2xl px-4 py-3 text-sm font-bold text-slate-800 outline-none focus:border-[#003317] transition-all"
/>
</div>
<div className="space-y-1.5">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Reward Tokens</label>
<input
required
type="number"
placeholder="Amount"
value={newCoupon.rewardAmount}
onChange={(e) => setNewCoupon({ ...newCoupon, rewardAmount: e.target.value })}
className="w-full bg-slate-50 border border-slate-100 rounded-2xl px-4 py-3 text-sm font-bold text-slate-800 outline-none focus:border-[#003317] transition-all"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-6">
<div className="space-y-1.5">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Expiry Calendar</label>
<input
required
type="date"
value={newCoupon.expiryDate}
onChange={(e) => setNewCoupon({ ...newCoupon, expiryDate: e.target.value })}
className="w-full bg-slate-50 border border-slate-100 rounded-2xl px-4 py-3 text-sm font-bold text-slate-800 outline-none focus:border-[#003317] transition-all"
/>
</div>
<div className="space-y-1.5">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Quota (Max Claims)</label>
<input
required
type="number"
placeholder="Inventory Limit"
value={newCoupon.maxClaims}
onChange={(e) => setNewCoupon({ ...newCoupon, maxClaims: e.target.value })}
className="w-full bg-slate-50 border border-slate-100 rounded-2xl px-4 py-3 text-sm font-bold text-slate-800 outline-none focus:border-[#003317] transition-all"
/>
</div>
</div>
<div className="space-y-1.5">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Campaign Description</label>
<textarea
placeholder="What is this code for?"
rows={3}
value={newCoupon.description}
onChange={(e) => setNewCoupon({ ...newCoupon, description: e.target.value })}
className="w-full bg-slate-50 border border-slate-100 rounded-2xl px-4 py-3 text-sm font-bold text-slate-800 outline-none focus:border-[#003317] transition-all resize-none"
/>
</div>
<button
type="submit"
className="w-full bg-[#003317] text-white py-4 rounded-2xl text-xs font-black uppercase tracking-widest shadow-xl shadow-[#003317]/20 hover:scale-[1.02] active:scale-[0.98] transition-all mt-4 flex items-center justify-center gap-3"
>
Confirm Generation
<ChevronRight size={18} />
</button>
</form>
</motion.div>
</div>
)}
</AnimatePresence>
</div>
{/* Creation Modal */}
<AnimatePresence>
{isModalOpen && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setIsModalOpen(false)}
className="absolute inset-0 bg-[#0f4475]/40 backdrop-blur-md"
/>
<motion.div
initial={{ scale: 0.9, opacity: 0, y: 20 }}
animate={{ scale: 1, opacity: 1, y: 0 }}
exit={{ scale: 0.9, opacity: 0, y: 20 }}
className="bg-white w-full max-w-lg rounded-[3rem] p-8 shadow-2xl relative z-10 border border-white/20"
>
<div className="flex justify-between items-center mb-8">
<div className="flex items-center gap-3">
<div className="p-2 bg-indigo-50 text-[#0f4475] rounded-xl">
<Ticket size={24} />
</div>
<h2 className="text-xl font-black text-slate-800 uppercase tracking-tight">Generate Code</h2>
</div>
<button onClick={() => setIsModalOpen(false)} className="p-2 hover:bg-slate-100 rounded-full transition-colors">
<X size={20} className="text-slate-400" />
</button>
</div>
<form onSubmit={handleCreateCoupon} className="space-y-6">
<div className="grid grid-cols-2 gap-6">
<div className="space-y-1.5">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Reference Code</label>
<input
required
type="text"
placeholder="e.g. WELCOME50"
value={newCoupon.code}
onChange={(e) => setNewCoupon({...newCoupon, code: e.target.value.toUpperCase()})}
className="w-full bg-slate-50 border border-slate-100 rounded-2xl px-4 py-3 text-sm font-bold text-slate-800 outline-none focus:border-[#0f4475] transition-all"
/>
</div>
<div className="space-y-1.5">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Reward Tokens</label>
<input
required
type="number"
placeholder="Amount"
value={newCoupon.rewardAmount}
onChange={(e) => setNewCoupon({...newCoupon, rewardAmount: e.target.value})}
className="w-full bg-slate-50 border border-slate-100 rounded-2xl px-4 py-3 text-sm font-bold text-slate-800 outline-none focus:border-[#0f4475] transition-all"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-6">
<div className="space-y-1.5">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Expiry Calendar</label>
<input
required
type="date"
value={newCoupon.expiryDate}
onChange={(e) => setNewCoupon({...newCoupon, expiryDate: e.target.value})}
className="w-full bg-slate-50 border border-slate-100 rounded-2xl px-4 py-3 text-sm font-bold text-slate-800 outline-none focus:border-[#0f4475] transition-all"
/>
</div>
<div className="space-y-1.5">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Quota (Max Claims)</label>
<input
required
type="number"
placeholder="Inventory Limit"
value={newCoupon.maxClaims}
onChange={(e) => setNewCoupon({...newCoupon, maxClaims: e.target.value})}
className="w-full bg-slate-50 border border-slate-100 rounded-2xl px-4 py-3 text-sm font-bold text-slate-800 outline-none focus:border-[#0f4475] transition-all"
/>
</div>
</div>
<div className="space-y-1.5">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Campaign Description</label>
<textarea
placeholder="What is this code for?"
rows={3}
value={newCoupon.description}
onChange={(e) => setNewCoupon({...newCoupon, description: e.target.value})}
className="w-full bg-slate-50 border border-slate-100 rounded-2xl px-4 py-3 text-sm font-bold text-slate-800 outline-none focus:border-[#0f4475] transition-all resize-none"
/>
</div>
<button
type="submit"
className="w-full bg-[#0f4475] text-white py-4 rounded-2xl text-xs font-black uppercase tracking-widest shadow-xl shadow-[#0f4475]/20 hover:scale-[1.02] active:scale-[0.98] transition-all mt-4 flex items-center justify-center gap-3"
>
Confirm Generation
<ChevronRight size={18} />
</button>
</form>
</motion.div>
</div>
)}
</AnimatePresence>
</div>
);
);
};
export default ManageCoupons;

View File

@@ -97,7 +97,7 @@ const ManageWallets = () => {
<div className="flex items-center justify-between">
<div className="flex flex-col">
<div className="flex items-center gap-2 mb-1">
<h2 className="text-xs font-black text-[#0f4475] uppercase tracking-widest">Ritz Section</h2>
<h2 className="text-xs font-black text-[#003317] uppercase tracking-widest">Ritz Section</h2>
</div>
<h1 className="text-2xl font-black text-slate-800">Manage Wallets</h1>
<p className="text-[10px] text-slate-400 font-medium flex items-center gap-1.5 mt-1">
@@ -114,7 +114,7 @@ const ManageWallets = () => {
placeholder="Search user or mobile..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="bg-white border border-slate-200 rounded-xl pl-10 pr-4 py-2 text-sm outline-none focus:border-[#0f4475] transition-all w-64 shadow-sm"
className="bg-white border border-slate-200 rounded-xl pl-10 pr-4 py-2 text-sm outline-none focus:border-[#003317] transition-all w-64 shadow-sm"
/>
</div>
</div>
@@ -140,7 +140,7 @@ const ManageWallets = () => {
<motion.div
animate={{ scale: [1, 1.05, 1], opacity: [0.5, 1, 0.5] }}
transition={{ repeat: Infinity, duration: 1.5 }}
className="text-[10px] font-black text-[#0f4475] uppercase tracking-widest"
className="text-[10px] font-black text-[#003317] uppercase tracking-widest"
>
Loading Wallet Data...
</motion.div>
@@ -158,7 +158,7 @@ const ManageWallets = () => {
<td className="px-8 py-5 text-sm font-black text-slate-400 tabular-nums">#{user.id}</td>
<td className="px-8 py-5">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-[#0f4475]/5 flex items-center justify-center text-[#0f4475]">
<div className="w-10 h-10 rounded-xl bg-[#003317]/5 flex items-center justify-center text-[#003317]">
<UserIcon size={18} />
</div>
<div>
@@ -185,7 +185,7 @@ const ManageWallets = () => {
setSelectedUser(user);
setIsModalOpen(true);
}}
className="bg-[#0f4475] text-white px-4 py-2 rounded-xl text-[10px] font-black uppercase tracking-widest hover:scale-105 transition-all shadow-lg shadow-[#0f4475]/10 active:scale-95"
className="bg-[#003317] text-white px-4 py-2 rounded-xl text-[10px] font-black uppercase tracking-widest hover:scale-105 transition-all shadow-lg shadow-[#003317]/10 active:scale-95"
>
Credit Wallet
</button>
@@ -218,7 +218,7 @@ const ManageWallets = () => {
<div className="p-8">
<div className="flex items-center justify-between mb-8">
<div className="flex items-center gap-3">
<div className="p-2 bg-[#0f4475]/10 text-[#0f4475] rounded-xl">
<div className="p-2 bg-[#003317]/10 text-[#003317] rounded-xl">
<Plus size={20} />
</div>
<h3 className="text-lg font-black text-slate-800">Credit Ritz Tokens</h3>
@@ -244,13 +244,13 @@ const ManageWallets = () => {
<div>
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1 mb-2 block">Token Amount (Ritz)</label>
<div className="relative">
<div className="absolute left-4 top-1/2 -translate-y-1/2 font-black text-[#0f4475] text-lg">R</div>
<div className="absolute left-4 top-1/2 -translate-y-1/2 font-black text-[#003317] text-lg">R</div>
<input
type="number"
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="0.00"
className="w-full bg-slate-50 border border-slate-100 rounded-2xl pl-10 pr-4 py-4 text-2xl font-black text-slate-800 outline-none focus:bg-white focus:border-[#0f4475]/30 transition-all"
className="w-full bg-slate-50 border border-slate-100 rounded-2xl pl-10 pr-4 py-4 text-2xl font-black text-slate-800 outline-none focus:bg-white focus:border-[#003317]/30 transition-all"
required
autoFocus
/>
@@ -271,7 +271,7 @@ const ManageWallets = () => {
<button
type="submit"
disabled={isProcessing || !amount}
className="w-full bg-[#0f4475] text-white py-4 rounded-2xl font-black uppercase tracking-widest shadow-xl shadow-[#0f4475]/20 hover:scale-[1.02] transition-all flex items-center justify-center gap-2 group disabled:opacity-50 disabled:hover:scale-100"
className="w-full bg-[#003317] text-white py-4 rounded-2xl font-black uppercase tracking-widest shadow-xl shadow-[#003317]/20 hover:scale-[1.02] transition-all flex items-center justify-center gap-2 group disabled:opacity-50 disabled:hover:scale-100"
>
{isProcessing ? (
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />

View File

@@ -5,32 +5,32 @@ const NotFound = () => {
const navigate = useNavigate();
return (
<div className="min-h-screen bg-[#f8fafc] flex items-center justify-center p-6 font-sans selection:bg-[#0f4475]/10">
<div className="min-h-screen bg-[#f8fafc] flex items-center justify-center p-6 font-sans selection:bg-[#003317]/10">
<div className="max-w-4xl w-full">
<div className="grid lg:grid-cols-2 gap-12 items-center">
{/* Visual Section */}
<div className="relative">
{/* Background Decorative Circles */}
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-80 h-80 bg-[#0f4475]/5 rounded-full blur-3xl" />
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-80 h-80 bg-[#003317]/5 rounded-full blur-3xl" />
<div className="absolute top-1/4 left-1/4 w-32 h-32 bg-indigo-500/10 rounded-full blur-2xl" />
{/* 404 Main Text */}
<div className="relative">
<h1 className="text-[180px] font-black leading-none tracking-tighter text-[#0f4475] opacity-20 select-none">
<h1 className="text-[180px] font-black leading-none tracking-tighter text-[#003317] opacity-20 select-none">
404
</h1>
{/* Detailed Icon Stack */}
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 flex items-center justify-center">
<div className="p-8 bg-white rounded-[40px] shadow-2xl border border-white/50 backdrop-blur-xl relative z-10">
<Search size={80} className="text-[#0f4475]" strokeWidth={1.5} />
<Search size={80} className="text-[#003317]" strokeWidth={1.5} />
{/* Small Floating Icons */}
<div className="absolute -top-4 -right-4 p-3 bg-white rounded-2xl shadow-lg border border-white">
<HelpCircle size={24} className="text-indigo-500" />
</div>
<div className="absolute -bottom-6 -left-6 p-4 bg-[#0f4475] rounded-3xl shadow-xl border-4 border-white text-white">
<div className="absolute -bottom-6 -left-6 p-4 bg-[#003317] rounded-3xl shadow-xl border-4 border-white text-white">
<span className="text-sm font-black tracking-widest uppercase">Lost?</span>
</div>
</div>
@@ -48,7 +48,7 @@ const NotFound = () => {
</div>
<h2 className="text-4xl lg:text-5xl font-black text-[#1e293b] mb-6 leading-tight">
Well, this is <span className="text-transparent bg-clip-text bg-gradient-to-r from-[#0f4475] to-indigo-600">awkward.</span>
Well, this is <span className="text-transparent bg-clip-text bg-gradient-to-r from-[#003317] to-indigo-600">awkward.</span>
</h2>
<p className="text-[#64748b] text-lg mb-10 leading-relaxed font-medium">
@@ -58,7 +58,7 @@ const NotFound = () => {
<div className="flex flex-col sm:flex-row items-center gap-4 justify-center lg:justify-start">
<button
onClick={() => navigate('/dashboard')}
className="group px-8 py-4 bg-[#0f4475] text-white rounded-2xl font-bold flex items-center gap-3 shadow-xl shadow-[#0f4475]/20 hover:bg-[#1a5a92] transition-all hover:-translate-y-1 active:translate-y-0"
className="group px-8 py-4 bg-[#003317] text-white rounded-2xl font-bold flex items-center gap-3 shadow-xl shadow-[#003317]/20 hover:bg-[#1a5a92] transition-all hover:-translate-y-1 active:translate-y-0"
>
<Home size={18} className="group-hover:scale-110 transition-transform" />
Return to Safety
@@ -81,9 +81,9 @@ const NotFound = () => {
<button
key={item}
onClick={() => navigate(`/${item.toLowerCase()}`)}
className="text-xs font-bold text-[#64748b] hover:text-[#0f4475] transition-colors flex items-center gap-1 group"
className="text-xs font-bold text-[#64748b] hover:text-[#003317] transition-colors flex items-center gap-1 group"
>
<div className="w-1 h-1 rounded-full bg-[#e2e8f0] group-hover:bg-[#0f4475] transition-colors" />
<div className="w-1 h-1 rounded-full bg-[#e2e8f0] group-hover:bg-[#003317] transition-colors" />
{item}
</button>
))}

View File

@@ -170,7 +170,7 @@ const PurchaseAnalytics = () => {
if (isLoading) {
return (
<div className="h-full flex flex-col items-center justify-center gap-4">
<Loader2 className="animate-spin text-[#0f4475]" size={40} />
<Loader2 className="animate-spin text-[#003317]" size={40} />
<p className="text-sm font-bold text-slate-500 uppercase tracking-widest">Calculating Market Analytics...</p>
</div>
);
@@ -181,12 +181,12 @@ const PurchaseAnalytics = () => {
{/* Header */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-black text-[#0f4475] tracking-tight uppercase">Purchase Analytics</h1>
<h1 className="text-2xl font-black text-[#003317] tracking-tight uppercase">Purchase Analytics</h1>
<div className="flex items-center gap-2 mt-1">
<span className="bg-[#0f4475]/10 text-[#0f4475] text-[10px] font-bold px-2 py-0.5 rounded-full uppercase">
<span className="bg-[#003317]/10 text-[#003317] text-[10px] font-bold px-2 py-0.5 rounded-full uppercase">
Start: {format(dateRange.start, 'yyyy-MM-dd')}
</span>
<span className="bg-[#0f4475]/10 text-[#0f4475] text-[10px] font-bold px-2 py-0.5 rounded-full uppercase">
<span className="bg-[#003317]/10 text-[#003317] text-[10px] font-bold px-2 py-0.5 rounded-full uppercase">
End: {format(dateRange.end, 'yyyy-MM-dd')}
</span>
</div>
@@ -194,7 +194,7 @@ const PurchaseAnalytics = () => {
<div className="flex items-center gap-3">
<button
onClick={() => setFilterType('all')}
className={`px-4 py-2 rounded-xl text-xs font-bold uppercase transition-all ${filterType === 'all' ? 'bg-[#0f4475] text-white shadow-lg' : 'bg-white border text-slate-500 border-slate-200 hover:bg-slate-50'}`}
className={`px-4 py-2 rounded-xl text-xs font-bold uppercase transition-all ${filterType === 'all' ? 'bg-[#003317] text-white shadow-lg' : 'bg-white border text-slate-500 border-slate-200 hover:bg-slate-50'}`}
>
Show All
</button>
@@ -231,7 +231,7 @@ const PurchaseAnalytics = () => {
<input
type="text"
placeholder="Search products..."
className="pl-9 pr-4 py-2 border border-[#e2e8f0] rounded-xl text-xs outline-none focus:border-[#0f4475]/30 transition-all font-medium"
className="pl-9 pr-4 py-2 border border-[#e2e8f0] rounded-xl text-xs outline-none focus:border-[#003317]/30 transition-all font-medium"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
@@ -254,7 +254,7 @@ const PurchaseAnalytics = () => {
) : (
filteredAnalytics.map((item, idx) => (
<tr key={idx} className="hover:bg-gray-50/50 transition-colors group">
<td className="px-6 py-4"><p className="text-xs font-bold text-[#1e293b] group-hover:text-[#0f4475]">{item.product}</p></td>
<td className="px-6 py-4"><p className="text-xs font-bold text-[#1e293b] group-hover:text-[#003317]">{item.product}</p></td>
<td className="px-6 py-4 text-xs font-bold text-[#475569]">{item.qty}</td>
<td className="px-6 py-4 text-xs font-bold text-[#475569] text-right">{item.basePrice.toFixed(2)}</td>
<td className="px-6 py-4 text-right">
@@ -286,7 +286,7 @@ const PurchaseAnalytics = () => {
<div className="grid grid-cols-2 gap-4">
<button
onClick={() => setFilterType('up')}
className={`p-4 rounded-2xl flex flex-col items-center justify-center gap-2 transition-all cursor-pointer hover:border-[#0f4475]/50 border-2 ${filterType === 'up' ? 'bg-[#0f4475] text-white border-[#0f4475]' : 'bg-gray-50 text-slate-400 border-transparent hover:bg-slate-100'}`}
className={`p-4 rounded-2xl flex flex-col items-center justify-center gap-2 transition-all cursor-pointer hover:border-[#003317]/50 border-2 ${filterType === 'up' ? 'bg-[#003317] text-white border-[#003317]' : 'bg-gray-50 text-slate-400 border-transparent hover:bg-slate-100'}`}
>
<TrendingUp size={20} />
<span className="text-[10px] font-bold uppercase tracking-wider">Inflation</span>

View File

@@ -1,220 +1,220 @@
import { apiFetch } from '../api';
import { useState, useEffect } from 'react';
import {
Filter,
ArrowRight,
Search,
CheckCircle2,
AlertCircle
Filter,
ArrowRight,
Search,
CheckCircle2,
AlertCircle
} from 'lucide-react';
import {
AreaChart,
Area,
XAxis,
YAxis,
Tooltip,
ResponsiveContainer
AreaChart,
Area,
XAxis,
YAxis,
Tooltip,
ResponsiveContainer
} from 'recharts';
import { motion } from 'framer-motion';
const PurchaseSummary = () => {
const [activeRange, setActiveRange] = useState('Today');
const [data, setData] = useState<any>(null);
const [bills, setBills] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [activeRange, setActiveRange] = useState('Today');
const [data, setData] = useState<any>(null);
const [bills, setBills] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
try {
setIsLoading(true);
// Fetch summary and bills in parallel
const [summaryRes, billsRes] = await Promise.all([
apiFetch('/api/purchases/summary'),
apiFetch('/api/purchases/orders')
]);
useEffect(() => {
const fetchData = async () => {
try {
setIsLoading(true);
// Fetch summary and bills in parallel
const [summaryRes, billsRes] = await Promise.all([
apiFetch('/api/purchases/summary'),
apiFetch('/api/purchases/orders')
]);
if (summaryRes.ok && billsRes.ok) {
const summaryData = await summaryRes.json();
const billsData = await billsRes.json();
setData(summaryData);
setBills(billsData);
}
} catch (error) {
console.error('Error fetching purchase summary:', error);
} finally {
setIsLoading(false);
}
};
fetchData();
}, []);
if (summaryRes.ok && billsRes.ok) {
const summaryData = await summaryRes.json();
const billsData = await billsRes.json();
setData(summaryData);
setBills(billsData);
}
} catch (error) {
console.error('Error fetching purchase summary:', error);
} finally {
setIsLoading(false);
}
};
fetchData();
}, []);
if (isLoading || !data) {
return (
<div className="h-screen flex items-center justify-center bg-slate-50/50">
<motion.div
animate={{ scale: [1, 1.05, 1] }}
transition={{ repeat: Infinity, duration: 2 }}
className="text-[#0f4475] font-black uppercase tracking-widest text-xs"
>
Compiling Fiscal Intelligence...
</motion.div>
</div>
);
}
if (isLoading || !data) {
return (
<div className="h-screen flex items-center justify-center bg-slate-50/50">
<motion.div
animate={{ scale: [1, 1.05, 1] }}
transition={{ repeat: Infinity, duration: 2 }}
className="text-[#003317] font-black uppercase tracking-widest text-xs"
>
Compiling Fiscal Intelligence...
</motion.div>
</div>
);
}
const summaryStats = [
{ title: "Total Purchases", value: `${(data?.totalAmount ?? 0).toLocaleString()}`, icon: AlertCircle, color: "text-rose-500", bg: "bg-rose-50" },
{ title: "Total Credits", value: `${(data?.balanceAmount ?? 0).toLocaleString()}`, icon: AlertCircle, color: "text-amber-500", bg: "bg-amber-50" },
{ title: "Credit Paid", value: `${(data?.paidAmount ?? 0).toLocaleString()}`, icon: AlertCircle, color: "text-emerald-500", bg: "bg-emerald-50" },
{ title: "Balance Credit", value: `${(data?.balanceAmount ?? 0).toLocaleString()}`, icon: AlertCircle, color: "text-indigo-500", bg: "bg-indigo-50" },
{ title: "Bills to Pay", value: (data?.unpaidCount ?? 0).toString(), icon: AlertCircle, color: "text-blue-500", bg: "bg-blue-50" }
];
const summaryStats = [
{ title: "Total Purchases", value: `${(data?.totalAmount ?? 0).toLocaleString()}`, icon: AlertCircle, color: "text-rose-500", bg: "bg-rose-50" },
{ title: "Total Credits", value: `${(data?.balanceAmount ?? 0).toLocaleString()}`, icon: AlertCircle, color: "text-amber-500", bg: "bg-amber-50" },
{ title: "Credit Paid", value: `${(data?.paidAmount ?? 0).toLocaleString()}`, icon: AlertCircle, color: "text-emerald-500", bg: "bg-emerald-50" },
{ title: "Balance Credit", value: `${(data?.balanceAmount ?? 0).toLocaleString()}`, icon: AlertCircle, color: "text-indigo-500", bg: "bg-indigo-50" },
{ title: "Bills to Pay", value: (data?.unpaidCount ?? 0).toString(), icon: AlertCircle, color: "text-blue-500", bg: "bg-blue-50" }
];
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="Back" 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-[#0f4475] uppercase tracking-widest">Purchases</h2>
</div>
<h1 className="text-2xl font-black text-slate-800">Summary</h1>
</div>
<div className="flex items-center gap-2 bg-white p-1 rounded-2xl border border-slate-200 shadow-sm">
{['Yesterday', 'Today', 'Week', '30 Days'].map(range => (
<button
key={range}
onClick={() => setActiveRange(range)}
className={`px-4 py-2 text-[10px] font-black uppercase tracking-widest transition-all rounded-xl ${activeRange === range ? 'text-white bg-[#0f4475] shadow-md shadow-[#0f4475]/20' : 'text-slate-400 hover:text-slate-600'}`}
>
{range}
</button>
))}
</div>
</div>
<div className="grid grid-cols-12 gap-8">
<div className="col-span-12 lg:col-span-5 space-y-6">
<div className="grid grid-cols-2 gap-4">
{summaryStats.map((stat, idx) => (
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: idx * 0.05 }}
key={idx}
whileHover={{ y: -4 }}
className="bg-white p-6 rounded-[28px] border border-slate-100 shadow-sm transition-all"
>
<div className="flex items-center gap-3 mb-4">
<div className={`p-2.5 rounded-xl ${stat.bg} ${stat.color}`}>
<stat.icon size={20} />
</div>
<h3 className="text-[9px] font-black text-slate-400 uppercase tracking-widest leading-none">{stat.title}</h3>
</div>
<p className="text-2xl font-black text-slate-800">{stat.value}</p>
</motion.div>
))}
<div className="col-span-2 bg-[#0f4475] p-6 rounded-[32px] shadow-lg shadow-[#0f4475]/20 text-white relative overflow-hidden group">
<div className="absolute -right-8 -top-8 w-32 h-32 bg-white/10 rounded-full blur-3xl" />
<div className="relative z-10 flex items-end justify-between">
<div>
<h3 className="text-[10px] font-black uppercase tracking-widest opacity-80 text-white/70 mb-8">Bill Fulfillment</h3>
<p className="text-4xl font-black mb-1">{bills.filter(b => b.status === 'PAID').length}</p>
<p className="text-[10px] font-bold uppercase tracking-widest opacity-60">Cleared Bills</p>
</div>
<div className="text-right">
<CheckCircle2 size={32} className="text-emerald-400 mb-6 ml-auto" />
<p className="text-lg font-black text-emerald-400">
{bills.length > 0 ? Math.round((bills.filter(b => b.status === 'PAID').length / bills.length) * 100) : 0}%
</p>
<p className="text-[9px] font-bold uppercase tracking-widest opacity-60">Success Rate</p>
</div>
</div>
</div>
</div>
</div>
<div className="col-span-12 lg:col-span-7 space-y-8">
<div className="bg-white p-8 rounded-[40px] border border-slate-100 shadow-sm h-full">
<div className="flex items-center justify-between mb-8">
<h3 className="text-sm font-black text-slate-800 tracking-tight uppercase tracking-widest">Purchase Trends</h3>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-[#f43f5e]" />
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Procurement Volume</span>
</div>
</div>
<div className="h-[280px]">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={data?.trend}>
<defs>
<linearGradient id="purchaseGradSum" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#f43f5e" stopOpacity={0.1}/>
<stop offset="95%" stopColor="#f43f5e" stopOpacity={0}/>
</linearGradient>
</defs>
<XAxis dataKey="date" 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}k`} />
<Tooltip contentStyle={{ borderRadius: '16px', border: 'none', boxShadow: '0 10px 25px -5px rgb(0 0 0 / 0.1)' }} />
<Area type="monotone" dataKey="amount" stroke="#f43f5e" strokeWidth={4} fillOpacity={1} fill="url(#purchaseGradSum)" />
</AreaChart>
</ResponsiveContainer>
</div>
</div>
</div>
</div>
<div className="bg-white rounded-[32px] border border-slate-100 shadow-sm p-8 pb-12">
<div className="flex items-center justify-between mb-8">
<h3 className="text-sm font-black text-slate-800 tracking-tight uppercase tracking-widest">Recent Activity</h3>
<div className="flex items-center gap-4">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" size={14} />
<input type="text" title="Search" placeholder="Filter bills..." className="pl-9 pr-4 py-2 bg-slate-50 border border-slate-100 rounded-xl text-xs font-bold outline-none focus:border-[#0f4475] transition-all" />
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="Back" 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">Purchases</h2>
</div>
<button title="Filter" className="p-2.5 bg-slate-50 rounded-xl text-slate-500 border border-slate-100"><Filter size={16} /></button>
<h1 className="text-2xl font-black text-slate-800">Summary</h1>
</div>
<div className="flex items-center gap-2 bg-white p-1 rounded-2xl border border-slate-200 shadow-sm">
{['Yesterday', 'Today', 'Week', '30 Days'].map(range => (
<button
key={range}
onClick={() => setActiveRange(range)}
className={`px-4 py-2 text-[10px] font-black uppercase tracking-widest transition-all rounded-xl ${activeRange === range ? 'text-white bg-[#003317] shadow-md shadow-[#003317]/20' : 'text-slate-400 hover:text-slate-600'}`}
>
{range}
</button>
))}
</div>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="bg-slate-50/50">
<th className="px-6 py-4 text-left text-[10px] font-black text-slate-400 uppercase tracking-widest">Bill No</th>
<th className="px-6 py-4 text-left text-[10px] font-black text-slate-400 uppercase tracking-widest">Date</th>
<th className="px-6 py-4 text-left text-[10px] font-black text-slate-400 uppercase tracking-widest">Vendor</th>
<th className="px-6 py-4 text-left text-[10px] font-black text-slate-400 uppercase tracking-widest">Status</th>
<th className="px-6 py-4 text-right text-[10px] font-black text-slate-400 uppercase tracking-widest">Balance</th>
<th className="px-6 py-4 text-right text-[10px] font-black text-slate-400 uppercase tracking-widest">Amount</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
{bills.map((bill: any, idx: number) => (
<tr key={idx} className="hover:bg-slate-50/50 transition-all cursor-pointer group">
<td className="px-6 py-5 text-xs font-black text-slate-800">{bill.purchaseId}</td>
<td className="px-6 py-5 text-xs font-bold text-slate-400">{bill.date}</td>
<td className="px-6 py-5">
<span className="text-xs font-black text-slate-700">{bill.vendor?.name || 'Walk-in'}</span>
</td>
<td className="px-6 py-5">
<span className={`px-3 py-1.5 rounded-xl text-[9px] font-black uppercase tracking-widest ${bill.status === 'PAID' ? 'bg-emerald-50 text-emerald-600' : 'bg-rose-50 text-rose-600'}`}>
{bill.status}
</span>
</td>
<td className="px-6 py-5 text-right text-xs font-black text-rose-500">{(bill.balance ?? 0).toLocaleString()}</td>
<td className="px-6 py-5 text-right text-xs font-black text-[#0f4475]">{(bill.amount ?? 0).toLocaleString()}</td>
</tr>
<div className="grid grid-cols-12 gap-8">
<div className="col-span-12 lg:col-span-5 space-y-6">
<div className="grid grid-cols-2 gap-4">
{summaryStats.map((stat, idx) => (
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: idx * 0.05 }}
key={idx}
whileHover={{ y: -4 }}
className="bg-white p-6 rounded-[28px] border border-slate-100 shadow-sm transition-all"
>
<div className="flex items-center gap-3 mb-4">
<div className={`p-2.5 rounded-xl ${stat.bg} ${stat.color}`}>
<stat.icon size={20} />
</div>
<h3 className="text-[9px] font-black text-slate-400 uppercase tracking-widest leading-none">{stat.title}</h3>
</div>
<p className="text-2xl font-black text-slate-800">{stat.value}</p>
</motion.div>
))}
</tbody>
</table>
<div className="col-span-2 bg-[#003317] p-6 rounded-[32px] shadow-lg shadow-[#003317]/20 text-white relative overflow-hidden group">
<div className="absolute -right-8 -top-8 w-32 h-32 bg-white/10 rounded-full blur-3xl" />
<div className="relative z-10 flex items-end justify-between">
<div>
<h3 className="text-[10px] font-black uppercase tracking-widest opacity-80 text-white/70 mb-8">Bill Fulfillment</h3>
<p className="text-4xl font-black mb-1">{bills.filter(b => b.status === 'PAID').length}</p>
<p className="text-[10px] font-bold uppercase tracking-widest opacity-60">Cleared Bills</p>
</div>
<div className="text-right">
<CheckCircle2 size={32} className="text-emerald-400 mb-6 ml-auto" />
<p className="text-lg font-black text-emerald-400">
{bills.length > 0 ? Math.round((bills.filter(b => b.status === 'PAID').length / bills.length) * 100) : 0}%
</p>
<p className="text-[9px] font-bold uppercase tracking-widest opacity-60">Success Rate</p>
</div>
</div>
</div>
</div>
</div>
<div className="col-span-12 lg:col-span-7 space-y-8">
<div className="bg-white p-8 rounded-[40px] border border-slate-100 shadow-sm h-full">
<div className="flex items-center justify-between mb-8">
<h3 className="text-sm font-black text-slate-800 tracking-tight uppercase tracking-widest">Purchase Trends</h3>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-[#f43f5e]" />
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Procurement Volume</span>
</div>
</div>
<div className="h-[280px]">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={data?.trend}>
<defs>
<linearGradient id="purchaseGradSum" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#f43f5e" stopOpacity={0.1} />
<stop offset="95%" stopColor="#f43f5e" stopOpacity={0} />
</linearGradient>
</defs>
<XAxis dataKey="date" 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}k`} />
<Tooltip contentStyle={{ borderRadius: '16px', border: 'none', boxShadow: '0 10px 25px -5px rgb(0 0 0 / 0.1)' }} />
<Area type="monotone" dataKey="amount" stroke="#f43f5e" strokeWidth={4} fillOpacity={1} fill="url(#purchaseGradSum)" />
</AreaChart>
</ResponsiveContainer>
</div>
</div>
</div>
</div>
<div className="bg-white rounded-[32px] border border-slate-100 shadow-sm p-8 pb-12">
<div className="flex items-center justify-between mb-8">
<h3 className="text-sm font-black text-slate-800 tracking-tight uppercase tracking-widest">Recent Activity</h3>
<div className="flex items-center gap-4">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" size={14} />
<input type="text" title="Search" placeholder="Filter bills..." className="pl-9 pr-4 py-2 bg-slate-50 border border-slate-100 rounded-xl text-xs font-bold outline-none focus:border-[#003317] transition-all" />
</div>
<button title="Filter" className="p-2.5 bg-slate-50 rounded-xl text-slate-500 border border-slate-100"><Filter size={16} /></button>
</div>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="bg-slate-50/50">
<th className="px-6 py-4 text-left text-[10px] font-black text-slate-400 uppercase tracking-widest">Bill No</th>
<th className="px-6 py-4 text-left text-[10px] font-black text-slate-400 uppercase tracking-widest">Date</th>
<th className="px-6 py-4 text-left text-[10px] font-black text-slate-400 uppercase tracking-widest">Vendor</th>
<th className="px-6 py-4 text-left text-[10px] font-black text-slate-400 uppercase tracking-widest">Status</th>
<th className="px-6 py-4 text-right text-[10px] font-black text-slate-400 uppercase tracking-widest">Balance</th>
<th className="px-6 py-4 text-right text-[10px] font-black text-slate-400 uppercase tracking-widest">Amount</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
{bills.map((bill: any, idx: number) => (
<tr key={idx} className="hover:bg-slate-50/50 transition-all cursor-pointer group">
<td className="px-6 py-5 text-xs font-black text-slate-800">{bill.purchaseId}</td>
<td className="px-6 py-5 text-xs font-bold text-slate-400">{bill.date}</td>
<td className="px-6 py-5">
<span className="text-xs font-black text-slate-700">{bill.vendor?.name || 'Walk-in'}</span>
</td>
<td className="px-6 py-5">
<span className={`px-3 py-1.5 rounded-xl text-[9px] font-black uppercase tracking-widest ${bill.status === 'PAID' ? 'bg-emerald-50 text-emerald-600' : 'bg-rose-50 text-rose-600'}`}>
{bill.status}
</span>
</td>
<td className="px-6 py-5 text-right text-xs font-black text-rose-500">{(bill.balance ?? 0).toLocaleString()}</td>
<td className="px-6 py-5 text-right text-xs font-black text-[#003317]">{(bill.amount ?? 0).toLocaleString()}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
);
);
};
export default PurchaseSummary;

View File

@@ -1,405 +1,405 @@
import { apiFetch } from '../api';
import { useState, useEffect } from 'react';
import {
ChevronRight,
ShoppingBag,
Wallet,
ArrowRight,
RotateCcw,
Star,
UserMinus
ChevronRight,
ShoppingBag,
Wallet,
ArrowRight,
RotateCcw,
Star,
UserMinus
} from 'lucide-react';
import {
AreaChart,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
PieChart,
Pie,
Cell
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 [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 [activeTab, setActiveTab] = useState('Sales');
const [timeRange, setTimeRange] = useState('Today');
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 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 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());
};
};
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();
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);
// 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;
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);
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.totalSales,
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: ritStore.sale,
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);
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;
default:
start.setHours(0, 0, 0, 0);
}
};
fetchStats();
}, [timeRange]);
return { from: toLocalISOString(start), to: toLocalISOString(end) };
};
const pieData = [
{ name: 'Full Payment', value: Number(stats.totalSales) || 0, color: '#8b5cf6' },
{ name: 'Credit', value: 0, color: '#fbbf24' }
];
useEffect(() => {
const fetchStats = async () => {
try {
setIsLoading(true);
const range = getRangeDates(timeRange);
const params = new URLSearchParams();
params.append('from', range.from);
params.append('to', range.to);
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-[#0f4475] font-black uppercase tracking-widest text-sm"
>
Analyzing Store Data...
</motion.div>
</div>
);
}
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.totalSales,
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: ritStore.sale,
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]);
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-[#0f4475] 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-[#0f4475]' : '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-[#0f4475]" />
)}
</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">R{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 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-[#0f4475]' : '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-[#0f4475]" />
)}
</button>
))}
</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-[#0f4475]/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-[#0f4475]/30 transition-all">
<div className="flex items-center gap-3">
<div className="p-2 bg-amber-100 text-[#0f4475] 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>
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="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-[#0f4475] 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 className="flex items-center gap-3">
{/* Action buttons or profile placeholder can go here if needed later */}
</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>
{/* 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="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 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">R{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>
<p className="text-[11px] font-bold leading-relaxed">{insight.text}</p>
</div>
))}
</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-[#0f4475]/5 hover:bg-[#0f4475]/10 text-[#0f4475] 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>
{/* Right Stats Column */}
<div className="col-span-12 lg:col-span-3 space-y-6">
{/* Filters */}
<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>
{/* 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>
</div>
);
);
};
export default StoreDashboard;

View File

@@ -21,13 +21,6 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const savedUser = localStorage.getItem('user');
if (savedUser) {
setUser(JSON.parse(savedUser));
}
setIsLoading(false);
}, []);
const checkUserExists = async (mobileNumber: string) => {
try {
@@ -157,11 +150,12 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
}
};
const refreshUser = async () => {
if (!user) return;
const refreshUser = async (userOverride?: User | null) => {
const targetUser = userOverride || user;
if (!targetUser) return;
try {
const token = localStorage.getItem('token');
const response = await fetch(`${API_BASE_URL}/user/${user.mobileNumber}`, {
const response = await fetch(`${API_BASE_URL}/user/${targetUser.mobileNumber}`, {
cache: 'no-store',
headers: {
...(token ? { 'Authorization': `Bearer ${token}` } : {})
@@ -179,8 +173,12 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
};
// Update state and localStorage
setUser(updatedUser);
localStorage.setItem('user', JSON.stringify(updatedUser));
if (updatedUser.isSuspended) {
logout();
} else {
setUser(updatedUser);
localStorage.setItem('user', JSON.stringify(updatedUser));
}
} else if (response.status === 404 || response.status === 401 || response.status === 403) {
logout();
}
@@ -189,6 +187,24 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
}
};
useEffect(() => {
const initAuth = async () => {
try {
const savedUser = localStorage.getItem('user');
if (savedUser) {
const parsedUser = JSON.parse(savedUser);
setUser(parsedUser);
await refreshUser(parsedUser);
}
} catch (error) {
console.error('Error during initial auth load:', error);
} finally {
setIsLoading(false);
}
};
initAuth();
}, []);
// Background status and balance sync
useEffect(() => {
if (!user) return;