HBT-software / src /components /Dashboard.tsx
embedingHF's picture
Upload folder using huggingface_hub
46463e1 verified
Raw
History Blame Contribute Delete
25.5 kB
/**
* @license
* SPDX-License-Identifier: Apache-2.0
*/
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('');
// 1. Calculate general statistics
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 => {
// Basic date checking
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);
// 2. Generate custom monthly line metrics for our 2026 sales chart
// Group invoices by month
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);
// 3. Category distribution
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);
// Reset counter
setReplenishAmount(prev => ({ ...prev, [productId]: 10 }));
};
// Filter products for low stock alert segment
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>
{/* Stock critical alerts and Quick Add Section */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6" id="dashboard-tables">
{/* Real-time low stock warnings widget */}
<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>
{/* Search alerts */}
<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>
{/* Recent Invoices Table list */}
<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>
);
}