| |
| |
| |
| |
|
|
| import React, { useState } from 'react'; |
| import { motion } from 'motion/react'; |
| import { |
| TrendingUp, |
| Package, |
| AlertTriangle, |
| FileText, |
| UserCheck, |
| Currency, |
| ArrowRight, |
| Plus, |
| RefreshCw, |
| Search, |
| ShoppingCart |
| } from 'lucide-react'; |
| import { TireProduct, Invoice, StaffUser, SystemSettings } from '../types'; |
|
|
| interface DashboardProps { |
| products: TireProduct[]; |
| invoices: Invoice[]; |
| currentStaff: StaffUser; |
| settings: SystemSettings; |
| setActiveTab: (tab: string) => void; |
| onQuickAddStock: (productId: string, amount: number) => void; |
| setSelectedInvoiceForView: (invoice: Invoice | null) => void; |
| } |
|
|
| export default function Dashboard({ |
| products, |
| invoices, |
| currentStaff, |
| settings, |
| setActiveTab, |
| onQuickAddStock, |
| setSelectedInvoiceForView |
| }: DashboardProps) { |
| const [replenishAmount, setReplenishAmount] = useState<{ [key: string]: number }>({}); |
| const [searchTerm, setSearchTerm] = useState(''); |
|
|
| |
| const totalInvoiced = invoices.reduce((sum, inv) => sum + (inv.paymentStatus === 'PAID' ? inv.total : 0), 0); |
| const totalActiveItems = products.reduce((sum, p) => sum + p.stock, 0); |
| const lowStockItems = products.filter(p => p.stock <= p.minStock); |
| const totalInvoicesCount = invoices.length; |
|
|
| const currentMonthName = "June 2026"; |
| const juneInvoices = invoices.filter(inv => { |
| |
| return inv.dateTime.includes('-06-') || inv.dateTime.includes('/06/') || inv.dateTime.startsWith('02-06-') || inv.dateTime.startsWith('03-06-'); |
| }); |
| const juneSalesTotal = juneInvoices.reduce((sum, inv) => sum + (inv.paymentStatus === 'PAID' ? inv.total : 0), 0); |
|
|
| |
| |
| const monthlyData = [ |
| { label: 'Jan', value: invoices.filter(i => i.dateTime.includes('-01-')).reduce((sum, i) => sum + i.total, 0) }, |
| { label: 'Feb', value: invoices.filter(i => i.dateTime.includes('-02-')).reduce((sum, i) => sum + i.total, 0) }, |
| { label: 'Mar', value: invoices.filter(i => i.dateTime.includes('-03-')).reduce((sum, i) => sum + i.total, 0) }, |
| { label: 'Apr', value: invoices.filter(i => i.dateTime.includes('-04-')).reduce((sum, i) => sum + i.total, 0) }, |
| { label: 'May', value: invoices.filter(i => i.dateTime.includes('-05-')).reduce((sum, i) => sum + i.total, 0) }, |
| { label: 'Jun', value: invoices.filter(i => i.dateTime.includes('-06-')).reduce((sum, i) => sum + i.total, 0) }, |
| ]; |
|
|
| const maxMonthlyVal = Math.max(...monthlyData.map(d => d.value), 100000); |
|
|
| |
| const categories = ['Passenger', 'SUV', 'Commercial', 'Light Truck', 'Truck/Bus'] as const; |
| const categoryStats = categories.map(cat => { |
| const items = products.filter(p => p.category === cat); |
| const count = items.reduce((sum, p) => sum + p.stock, 0); |
| const totalValue = items.reduce((sum, p) => sum + (p.stock * p.price), 0); |
| return { name: cat, count, value: totalValue }; |
| }); |
|
|
| const handleQuickAddClick = (productId: string) => { |
| const amt = replenishAmount[productId] || 10; |
| onQuickAddStock(productId, amt); |
| |
| setReplenishAmount(prev => ({ ...prev, [productId]: 10 })); |
| }; |
|
|
| |
| const filteredAlertProducts = lowStockItems.filter(p => |
| p.brand.toLowerCase().includes(searchTerm.toLowerCase()) || |
| p.model.toLowerCase().includes(searchTerm.toLowerCase()) || |
| p.size.toLowerCase().includes(searchTerm.toLowerCase()) |
| ); |
|
|
| return ( |
| <div className="space-y-6" id="dashboard-container"> |
| {/* Welcome Banner */} |
| <div className="bg-white border border-slate-200 rounded-xl p-6 md:p-8 text-slate-900 relative overflow-hidden flex flex-col md:flex-row justify-between items-start md:items-center gap-6 shadow-sm"> |
| <div className="relative z-10 space-y-2.5"> |
| <span className="bg-slate-100 text-slate-700 text-[10px] font-bold tracking-wider uppercase px-2.5 py-1 rounded-md border border-slate-200/60 inline-block font-sans"> |
| ★ Active Session — {currentStaff.role.toUpperCase()} MODE |
| </span> |
| <h1 className="text-2xl font-bold tracking-tight text-slate-900"> |
| {settings.shopName} |
| </h1> |
| <p className="text-slate-500 text-xs max-w-xl leading-relaxed"> |
| Direct invoicing, tax calculations, dynamic offline persistence, and real-time stock alerts. Welcomed operator: <span className="font-semibold text-slate-900">{currentStaff.name}</span>. |
| </p> |
| </div> |
| <div className="flex gap-2.5 relative z-10"> |
| <button |
| onClick={() => setActiveTab('invoices')} |
| className="bg-slate-900 hover:bg-slate-800 text-white text-xs font-medium px-4 py-2.5 rounded-lg transition duration-150 flex items-center gap-2 cursor-pointer shadow-sm" |
| > |
| <ShoppingCart className="w-3.5 h-3.5" /> |
| New Invoice |
| </button> |
| <button |
| onClick={() => setActiveTab('inventory')} |
| className="bg-white hover:bg-slate-50 text-slate-700 text-xs font-medium px-4 py-2.5 rounded-lg border border-slate-200 transition duration-150 flex items-center gap-2 cursor-pointer" |
| > |
| <Package className="w-3.5 h-3.5 text-slate-400" /> |
| Manage Tires |
| </button> |
| </div> |
| {/* Abstract tire background shadow */} |
| <div className="absolute right-[-40px] bottom-[-40px] opacity-5 pointer-events-none transform rotate-12"> |
| <div className="w-64 h-64 rounded-full border-[24px] border-slate-900 flex items-center justify-center"> |
| <div className="w-40 h-40 rounded-full border-[12px] border-slate-900 border-dashed"></div> |
| </div> |
| </div> |
| </div> |
| |
| {/* Main Core KPI Cards */} |
| <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5" id="kpi-grid"> |
| {/* Metric 1 */} |
| <div className="bg-white border border-slate-200 rounded-xl p-5 shadow-sm flex items-center justify-between"> |
| <div className="space-y-1"> |
| <p className="text-[10px] uppercase font-semibold text-slate-400 tracking-wider">Mtd Cash Flow ({currentMonthName})</p> |
| <p className="text-xl font-bold tracking-tight text-slate-900"> |
| {settings.currencySymbol} {juneSalesTotal.toLocaleString()} |
| </p> |
| <p className="text-[11px] text-slate-500 flex items-center gap-1"> |
| <span className="text-emerald-600 font-semibold flex items-center gap-0.5"> |
| <TrendingUp className="w-3 h-3" /> |
| +14.2% |
| </span> |
| <span>vs last month</span> |
| </p> |
| </div> |
| <div className="p-2.5 bg-slate-50 text-slate-800 border border-slate-150 rounded-lg"> |
| <TrendingUp className="w-4 h-4 text-slate-500" /> |
| </div> |
| </div> |
| |
| {/* Metric 2 */} |
| <div className="bg-white border border-slate-200 rounded-xl p-5 shadow-sm flex items-center justify-between"> |
| <div className="space-y-1"> |
| <p className="text-[10px] uppercase font-semibold text-slate-400 tracking-wider">Total Warehouse Stock</p> |
| <p className="text-xl font-bold tracking-tight text-slate-900"> |
| {totalActiveItems} pcs |
| </p> |
| <p className="text-[11px] text-slate-500"> |
| Across {products.length} distinct models |
| </p> |
| </div> |
| <div className="p-2.5 bg-slate-50 text-slate-800 border border-slate-150 rounded-lg"> |
| <Package className="w-4 h-4 text-slate-500" /> |
| </div> |
| </div> |
| |
| {/* Metric 3 */} |
| <div className="bg-white border border-slate-200 rounded-xl p-5 shadow-sm flex items-center justify-between"> |
| <div className="space-y-1"> |
| <p className="text-[10px] uppercase font-semibold text-slate-400 tracking-wider">Critical Alerts</p> |
| <p className="text-xl font-bold tracking-tight text-red-600"> |
| {lowStockItems.length} items |
| </p> |
| <p className="text-[11px] text-red-500 flex items-center gap-1 font-medium"> |
| <AlertTriangle className="w-3 h-3" /> |
| Urgent restock alert |
| </p> |
| </div> |
| <div className="p-2.5 bg-red-50 text-red-600 border border-red-100 rounded-lg"> |
| <AlertTriangle className="w-4 h-4 text-red-500" /> |
| </div> |
| </div> |
| |
| {/* Metric 4 */} |
| <div className="bg-white border border-slate-200 rounded-xl p-5 shadow-sm flex items-center justify-between"> |
| <div className="space-y-1"> |
| <p className="text-[10px] uppercase font-semibold text-slate-400 tracking-wider">Total Sales Invoices</p> |
| <p className="text-xl font-bold tracking-tight text-slate-900"> |
| {totalInvoicesCount} sales |
| </p> |
| <p className="text-[11px] text-slate-500"> |
| Invoiced: {settings.currencySymbol}{totalInvoiced.toLocaleString()} |
| </p> |
| </div> |
| <div className="p-2.5 bg-slate-50 text-slate-850 border border-slate-150 rounded-lg"> |
| <FileText className="w-4 h-4 text-slate-500" /> |
| </div> |
| </div> |
| </div> |
| |
| {/* Analytics SVG Graph Row */} |
| <div className="grid grid-cols-1 lg:grid-cols-3 gap-6" id="dashboard-analytics"> |
| {/* Core SVG Revenue Chart */} |
| <div className="bg-white border border-slate-200 rounded-xl p-5 shadow-sm lg:col-span-2 flex flex-col justify-between"> |
| <div className="flex justify-between items-center mb-6"> |
| <div> |
| <h3 className="text-sm font-bold text-slate-900 uppercase tracking-wide">Monthly Sales Reporting</h3> |
| <p className="text-xs text-slate-400">Telemetry of invoice turnover for year 2026</p> |
| </div> |
| <div className="bg-slate-50 text-slate-700 text-xs font-semibold px-2.5 py-1 rounded-md border border-slate-200"> |
| Year {new Date().getFullYear()} |
| </div> |
| </div> |
| |
| {/* SVG Animated Chart */} |
| <div className="relative h-64 w-full"> |
| <svg viewBox="0 0 600 240" className="w-full h-full overflow-visible"> |
| {/* Grid Lines */} |
| <line x1="40" y1="20" x2="580" y2="20" stroke="#f1f5f9" strokeWidth="1" /> |
| <line x1="40" y1="70" x2="580" y2="70" stroke="#f1f5f9" strokeWidth="1" /> |
| <line x1="40" y1="120" x2="580" y2="120" stroke="#f1f5f9" strokeWidth="1" /> |
| <line x1="40" y1="170" x2="580" y2="170" stroke="#f1f5f9" strokeWidth="1" /> |
| <line x1="40" y1="210" x2="580" y2="210" stroke="#e2e8f0" strokeWidth="1.5" /> |
| |
| {/* Chart line plotting */} |
| {(() => { |
| const getCoords = () => { |
| const xSpacing = 540 / 5; |
| return monthlyData.map((d, index) => { |
| const x = 40 + index * xSpacing; |
| // Scale value from 210 (bottom) to 20 (top) |
| const y = 210 - (d.value / maxMonthlyVal) * 180; |
| return { x, y, label: d.label, val: d.value }; |
| }); |
| }; |
| |
| const coords = getCoords(); |
| const pathD = coords.reduce((acc, c, idx) => { |
| return acc + (idx === 0 ? `M ${c.x} ${c.y}` : ` L ${c.x} ${c.y}`); |
| }, ""); |
| |
| const areaD = coords.reduce((acc, c, idx) => { |
| if (idx === 0) return `M ${c.x} 210 L ${c.x} ${c.y}`; |
| let ret = acc + ` L ${c.x} ${c.y}`; |
| if (idx === coords.length - 1) ret += ` L ${c.x} 210 Z`; |
| return ret; |
| }, ""); |
| |
| return ( |
| <> |
| {/* Shaded Area */} |
| <path d={areaD} fill="url(#blue-gradient)" opacity="0.1" /> |
| |
| {/* Connective Line */} |
| <path d={pathD} fill="none" stroke="#2563eb" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" /> |
| |
| {/* Dots and Labels */} |
| {coords.map((c, i) => ( |
| <g key={i} className="group cursor-pointer"> |
| {/* Hidden interactive target block */} |
| <circle cx={c.x} cy={c.y} r="14" fill="transparent" /> |
| <circle cx={c.x} cy={c.y} r="6" fill="#2563eb" stroke="#ffffff" strokeWidth="2" className="transition-all group-hover:scale-150 duration-100" /> |
| <text x={c.x} y="230" textAnchor="middle" className="text-[11px] font-medium fill-slate-500 font-sans">{c.label}</text> |
| {/* Turnover Value Overlay on Hover */} |
| <g className="opacity-0 group-hover:opacity-100 transition-opacity duration-150 pointer-events-none"> |
| <rect x={c.x - 55} y={c.y - 36} width="110" height="26" rx="6" fill="#0f172a" /> |
| <text x={c.x} y={c.y - 19} textAnchor="middle" className="text-[10px] font-semibold fill-white font-sans"> |
| {settings.currencySymbol}{Math.round(c.val).toLocaleString()} |
| </text> |
| </g> |
| </g> |
| ))} |
| |
| {/* Gradiant definitions */} |
| <defs> |
| <linearGradient id="blue-gradient" x1="0" y1="0" x2="0" y2="1"> |
| <stop offset="0%" stopColor="#2563eb" /> |
| <stop offset="100%" stopColor="#2563eb" stopOpacity="0" /> |
| </linearGradient> |
| </defs> |
| </> |
| ); |
| })()} |
| </svg> |
| </div> |
| |
| <div className="border-t border-slate-50 pt-4 flex justify-between text-xs text-slate-400"> |
| <span>Graph starts: Jan 2026</span> |
| <span>Real-time custom math calculation engine active</span> |
| <span>Graph terminal: June 2026</span> |
| </div> |
| </div> |
| |
| {/* Category Breakdown list */} |
| <div className="bg-white border border-slate-200 rounded-xl p-5 shadow-sm flex flex-col justify-between"> |
| <div> |
| <h3 className="text-sm font-bold text-slate-900 uppercase tracking-wide">Tire Category Audit</h3> |
| <p className="text-xs text-slate-400">Warehouse composition and valuation</p> |
| </div> |
| |
| <div className="space-y-4 my-6"> |
| {categoryStats.map((stat, i) => { |
| const maxCount = Math.max(...categoryStats.map(s => s.count), 1); |
| const percentage = Math.round((stat.count / maxCount) * 100); |
| |
| return ( |
| <div key={i} className="space-y-1"> |
| <div className="flex justify-between text-xs"> |
| <span className="font-semibold text-slate-700">{stat.name}</span> |
| <span className="text-slate-500">{stat.count} pcs — <span className="font-medium text-slate-800">{settings.currencySymbol}{stat.value.toLocaleString()}</span></span> |
| </div> |
| <div className="w-full bg-slate-100 h-2 rounded-full overflow-hidden"> |
| <div |
| className={`h-full rounded-full ${ |
| stat.name === 'Passenger' ? 'bg-blue-600' : |
| stat.name === 'SUV' ? 'bg-indigo-500' : |
| stat.name === 'Commercial' ? 'bg-emerald-500' : |
| stat.name === 'Light Truck' ? 'bg-amber-500' : 'bg-red-500' |
| }`} |
| style={{ width: `${percentage}%` }} |
| /> |
| </div> |
| </div> |
| ); |
| })} |
| </div> |
| |
| <div className="bg-slate-50 rounded-xl p-3 text-center border border-slate-100"> |
| <span className="text-xs font-semibold text-slate-600">Total Inventory Valuation:</span> |
| <p className="text-lg font-bold text-slate-900"> |
| {settings.currencySymbol} {products.reduce((sum, p) => sum + (p.stock * p.price), 0).toLocaleString()} |
| </p> |
| </div> |
| </div> |
| </div> |
|
|
| {} |
| <div className="grid grid-cols-1 lg:grid-cols-2 gap-6" id="dashboard-tables"> |
| {} |
| <div className="bg-white border border-slate-200 rounded-xl p-5 shadow-sm flex flex-col justify-between"> |
| <div className="space-y-2"> |
| <div className="flex justify-between items-center"> |
| <h3 className="text-sm font-bold text-slate-900 flex items-center gap-2 uppercase tracking-wide"> |
| <AlertTriangle className="w-4 h-4 text-slate-800" /> |
| Live Replenishment Alerts |
| </h3> |
| <span className="bg-red-50 text-red-800 text-[10px] font-bold px-2 py-0.5 rounded-md border border-red-100/60 font-sans"> |
| {lowStockItems.length} Low Stock |
| </span> |
| </div> |
| <p className="text-xs text-slate-500"> |
| The products listed below are currently equal to or below their alert limit. Top up immediately: |
| </p> |
| </div> |
|
|
| {} |
| <div className="my-4 relative"> |
| <span className="absolute left-3.5 top-3 text-slate-400"> |
| <Search className="w-4 h-4" /> |
| </span> |
| <input |
| type="text" |
| placeholder="Search low-stock products..." |
| value={searchTerm} |
| onChange={(e) => setSearchTerm(e.target.value)} |
| className="w-full text-xs border border-slate-200 outline-none focus:border-blue-500 bg-slate-50 hover:bg-slate-100 focus:bg-white rounded-xl pl-10 pr-4 py-2.5 transition duration-150" |
| /> |
| </div> |
|
|
| <div className="space-y-3 max-h-64 overflow-y-auto pr-1 my-2"> |
| {filteredAlertProducts.length === 0 ? ( |
| <div className="text-center py-8 border border-dashed border-slate-200 rounded-xl"> |
| <p className="text-sm text-slate-400">No matching low stock warnings!</p> |
| </div> |
| ) : ( |
| filteredAlertProducts.map((p) => { |
| const stockPercent = Math.max(Math.min((p.stock / p.minStock) * 100, 100), 5); |
| const currentReplValue = replenishAmount[p.id] || 10; |
| |
| return ( |
| <div key={p.id} className="border border-slate-100 hover:border-slate-200 rounded-xl p-3 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 bg-slate-50/50"> |
| <div className="space-y-1 col-span-2"> |
| <div className="flex items-center gap-2"> |
| <span className="text-xs font-bold text-slate-800">{p.brand} {p.model}</span> |
| <span className="text-[10px] bg-slate-200/70 text-slate-600 px-2 py-0.5 rounded font-mono">{p.size}</span> |
| </div> |
| <div className="flex items-center gap-3 text-xs"> |
| <span className="text-slate-500">Min. stock threshold: <span className="font-semibold text-slate-700">{p.minStock}</span></span> |
| <span className="text-slate-300">|</span> |
| <span className="text-red-600 font-bold">Qty: {p.stock} remaining</span> |
| </div> |
| |
| {/* Critical line representing the severe level */} |
| <div className="w-52 bg-slate-200 h-1 rounded-full overflow-hidden"> |
| <div className="h-full bg-red-500" style={{ width: `${stockPercent}%` }} /> |
| </div> |
| </div> |
| |
| {/* Stock quick load inputs */} |
| {['owner', 'manager'].includes(currentStaff.role) ? ( |
| <div className="flex items-center gap-2 w-full sm:w-auto justify-end"> |
| <input |
| type="number" |
| min="1" |
| max="200" |
| value={currentReplValue} |
| onChange={(e) => setReplenishAmount(prev => ({ ...prev, [p.id]: parseInt(e.target.value) || 1 }))} |
| className="w-16 border border-slate-200 focus:border-blue-500 bg-white shadow-inner outline-none rounded-lg text-xs py-1 px-2 font-mono text-center" |
| /> |
| <button |
| onClick={() => handleQuickAddClick(p.id)} |
| className="bg-blue-600 hover:bg-blue-700 font-bold text-white p-1.5 rounded-lg text-xs flex items-center gap-1 cursor-pointer transition shadow" |
| title="Restock now" |
| > |
| <Plus className="w-3.5 h-3.5" /> |
| Restock |
| </button> |
| </div> |
| ) : ( |
| <span className="text-[10px] text-slate-400 italic">Restock restricted to Managers</span> |
| )} |
| </div> |
| ); |
| }) |
| )} |
| </div> |
| <div className="pt-2 border-t border-slate-50 text-right"> |
| <button |
| onClick={() => setActiveTab('inventory')} |
| className="text-xs text-blue-600 hover:text-blue-800 flex items-center gap-1 ml-auto font-semibold cursor-pointer" |
| > |
| Examine full inventory history |
| <ArrowRight className="w-3.5 h-3.5" /> |
| </button> |
| </div> |
| </div> |
|
|
| {} |
| <div className="bg-white border border-slate-200 rounded-xl p-5 shadow-sm flex flex-col justify-between"> |
| <div className="space-y-1"> |
| <h3 className="text-sm font-bold text-slate-900 uppercase tracking-wide">Recent Transactions</h3> |
| <p className="text-xs text-slate-400">Review, query, and print historical customer receipts</p> |
| </div> |
|
|
| <div className="space-y-3 scrollbar-none overflow-y-auto max-h-80 pr-1 my-4"> |
| {invoices.length === 0 ? ( |
| <div className="text-center py-12 border border-dashed border-slate-200 rounded-xl"> |
| <p className="text-sm text-slate-400">No invoices drafted yet!</p> |
| </div> |
| ) : ( |
| [...invoices].reverse().slice(0, 5).map((inv) => ( |
| <div |
| key={inv.id} |
| onClick={() => setSelectedInvoiceForView(inv)} |
| className="group border border-slate-50 hover:border-blue-100 hover:bg-blue-50/20 rounded-xl p-3 flex justify-between items-center transition cursor-pointer" |
| > |
| <div className="space-y-1"> |
| <div className="flex items-center gap-2"> |
| <span className="font-mono text-xs font-semibold text-slate-900">{inv.invoiceNumber}</span> |
| {inv.isOfflineCreated && ( |
| <span className="bg-orange-100 text-orange-800 text-[9px] font-bold px-1.5 py-0.5 rounded-full" title="Saved locally offline. Ready for vault sync."> |
| OFFLINE |
| </span> |
| )} |
| </div> |
| <p className="text-xs font-medium text-slate-700">{inv.customerName}</p> |
| <p className="text-[10px] text-slate-400">{inv.dateTime}</p> |
| </div> |
| |
| <div className="text-right space-y-1"> |
| <span className="text-xs font-bold text-slate-900 font-sans"> |
| {inv.currencySymbol} {inv.total.toLocaleString()} |
| </span> |
| <div className="flex items-center gap-1 justify-end"> |
| <span className={`text-[10px] font-bold px-2 py-0.5 rounded ${ |
| inv.paymentStatus === 'PAID' ? 'bg-emerald-100 text-emerald-800' : 'bg-red-100 text-red-800' |
| }`}> |
| {inv.paymentStatus} |
| </span> |
| <span className="text-[9px] text-slate-400 bg-slate-100 px-1.5 rounded uppercase font-mono"> |
| {inv.paymentMethod} |
| </span> |
| </div> |
| </div> |
| </div> |
| )) |
| )} |
| </div> |
|
|
| <div className="pt-2 border-t border-slate-50"> |
| <button |
| onClick={() => setActiveTab('invoices')} |
| className="text-xs text-blue-600 hover:text-blue-800 flex items-center gap-1 ml-auto font-semibold cursor-pointer" |
| > |
| Access complete Invoices terminal |
| <ArrowRight className="w-3.5 h-3.5" /> |
| </button> |
| </div> |
| </div> |
| </div> |
| </div> |
| ); |
| } |
|
|