| |
| |
| |
| |
|
|
| import React, { useState, useEffect } from 'react'; |
| import { motion, AnimatePresence } from 'motion/react'; |
| import { |
| LayoutDashboard, |
| ShoppingCart, |
| Package, |
| Users, |
| Megaphone, |
| Settings, |
| Activity, |
| CloudLightning, |
| Wifi, |
| WifiOff, |
| Clock, |
| PhoneCall, |
| UserCircle |
| } from 'lucide-react'; |
|
|
| import { TireProduct, StockHistoryItem, StaffUser, Invoice, SystemSettings, StaffRole } from './types'; |
| import { |
| INITIAL_PRODUCTS, |
| INITIAL_HISTORY, |
| INITIAL_STAFF, |
| INITIAL_INVOICES, |
| INITIAL_SETTINGS |
| } from './initialData'; |
|
|
| |
| import Dashboard from './components/Dashboard'; |
| import InvoiceGenerator from './components/InvoiceGenerator'; |
| import InventoryManager from './components/InventoryManager'; |
| import StaffManager from './components/StaffManager'; |
| import SocialPromoter from './components/SocialPromoter'; |
| import SettingsComponent from './components/Settings'; |
|
|
| export default function App() { |
| const [activeTab, setActiveTab] = useState('dashboard'); |
| const [isOnline, setIsOnline] = useState(true); |
| const [currentTime, setCurrentTime] = useState(new Date()); |
|
|
| |
| const [settings, setSettings] = useState<SystemSettings>(INITIAL_SETTINGS); |
| const [products, setProducts] = useState<TireProduct[]>(INITIAL_PRODUCTS); |
| const [invoices, setInvoices] = useState<Invoice[]>(INITIAL_INVOICES); |
| const [history, setHistory] = useState<StockHistoryItem[]>(INITIAL_HISTORY); |
| const [staffList, setStaffList] = useState<StaffUser[]>(INITIAL_STAFF); |
| const [currentStaff, setCurrentStaff] = useState<StaffUser>(INITIAL_STAFF[1]); |
|
|
| |
| const [selectedInvoiceForView, setSelectedInvoiceForView] = useState<Invoice | null>(null); |
|
|
| |
| useEffect(() => { |
| |
| const timer = setInterval(() => setCurrentTime(new Date()), 1000); |
|
|
| const storedSettings = localStorage.getItem('HBT_SETTINGS'); |
| const storedProducts = localStorage.getItem('HBT_PRODUCTS'); |
| const storedInvoices = localStorage.getItem('HBT_INVOICES'); |
| const storedHistory = localStorage.getItem('HBT_HISTORY'); |
| const storedStaff = localStorage.getItem('HBT_STAFF'); |
| const storedCurrent = localStorage.getItem('HBT_CURRENT_STAFF'); |
|
|
| if (storedSettings) setSettings(JSON.parse(storedSettings)); |
| if (storedProducts) setProducts(JSON.parse(storedProducts)); |
| if (storedInvoices) setInvoices(JSON.parse(storedInvoices)); |
| if (storedHistory) setHistory(JSON.parse(storedHistory)); |
| if (storedStaff) setStaffList(JSON.parse(storedStaff)); |
| if (storedCurrent) { |
| const parsedCurrent = JSON.parse(storedCurrent); |
| |
| setCurrentStaff(parsedCurrent); |
| } else { |
| setCurrentStaff(INITIAL_STAFF[1]); |
| } |
|
|
| return () => clearInterval(timer); |
| }, []); |
|
|
| |
| const syncToLocalStorage = (key: string, data: any) => { |
| localStorage.setItem(key, JSON.stringify(data)); |
| }; |
|
|
| |
| const handleUpdateSettings = (updated: SystemSettings) => { |
| setSettings(updated); |
| syncToLocalStorage('HBT_SETTINGS', updated); |
| }; |
|
|
| |
| const handleQuickAddStock = (productId: string, amount: number) => { |
| const adjustedProducts = products.map(p => { |
| if (p.id === productId) { |
| const resultingStock = p.stock + amount; |
| |
| |
| const logItem: StockHistoryItem = { |
| id: 'log_' + Math.random().toString(36).substr(2, 9), |
| productId: p.id, |
| productLabel: `${p.brand} ${p.model} (${p.size})`, |
| dateTime: new Date().toISOString(), |
| type: 'STOCK_IN', |
| quantity: amount, |
| resultingStock, |
| adjustedBy: `${currentStaff.name} (${currentStaff.role})`, |
| reason: `Quick Restock of Alarm: Restocked cargo pallet.` |
| }; |
|
|
| const updatedHistory = [logItem, ...history]; |
| setHistory(updatedHistory); |
| syncToLocalStorage('HBT_HISTORY', updatedHistory); |
|
|
| return { ...p, stock: resultingStock }; |
| } |
| return p; |
| }); |
|
|
| setProducts(adjustedProducts); |
| syncToLocalStorage('HBT_PRODUCTS', adjustedProducts); |
| }; |
|
|
| |
| const handleAdjustProductStock = ( |
| productId: string, |
| quantityChange: number, |
| type: StockHistoryItem['type'], |
| reason: string |
| ) => { |
| const adjustedProducts = products.map(p => { |
| if (p.id === productId) { |
| const resultingStock = p.stock + quantityChange; |
|
|
| |
| const logItem: StockHistoryItem = { |
| id: 'log_' + Math.random().toString(36).substr(2, 9), |
| productId: p.id, |
| productLabel: `${p.brand} ${p.model} (${p.size})`, |
| dateTime: new Date().toISOString(), |
| type, |
| quantity: Math.abs(quantityChange), |
| resultingStock, |
| adjustedBy: `${currentStaff.name} (${currentStaff.role})`, |
| reason |
| }; |
|
|
| const updatedHistory = [logItem, ...history]; |
| setHistory(updatedHistory); |
| syncToLocalStorage('HBT_HISTORY', updatedHistory); |
|
|
| return { ...p, stock: resultingStock }; |
| } |
| return p; |
| }); |
|
|
| setProducts(adjustedProducts); |
| syncToLocalStorage('HBT_PRODUCTS', adjustedProducts); |
| }; |
|
|
| const handleAddNewProduct = (prod: TireProduct) => { |
| const updatedProducts = [prod, ...products]; |
| setProducts(updatedProducts); |
| syncToLocalStorage('HBT_PRODUCTS', updatedProducts); |
|
|
| |
| const logItem: StockHistoryItem = { |
| id: 'log_' + Math.random().toString(36).substr(2, 9), |
| productId: prod.id, |
| productLabel: `${prod.brand} ${prod.model} (${prod.size})`, |
| dateTime: new Date().toISOString(), |
| type: 'STOCK_IN', |
| quantity: prod.stock, |
| resultingStock: prod.stock, |
| adjustedBy: `${currentStaff.name} (${currentStaff.role})`, |
| reason: `First inventory system initialization of tyre profile SKU.` |
| }; |
|
|
| const updatedHistory = [logItem, ...history]; |
| setHistory(updatedHistory); |
| syncToLocalStorage('HBT_HISTORY', updatedHistory); |
| }; |
|
|
| const handleDeleteProduct = (productId: string) => { |
| const updatedProducts = products.filter(p => p.id !== productId); |
| setProducts(updatedProducts); |
| syncToLocalStorage('HBT_PRODUCTS', updatedProducts); |
| }; |
|
|
| |
| const handleAddInvoice = (newInv: Invoice) => { |
| |
| const updatedInvoices = [...invoices, newInv]; |
| setInvoices(updatedInvoices); |
| syncToLocalStorage('HBT_INVOICES', updatedInvoices); |
|
|
| |
| let revisedProducts = [...products]; |
| let newLogs: StockHistoryItem[] = []; |
|
|
| newInv.items.forEach((item) => { |
| revisedProducts = revisedProducts.map(p => { |
| if (p.id === item.productId) { |
| const resultingStock = p.stock - item.quantity; |
| |
| |
| const logItem: StockHistoryItem = { |
| id: 'log_' + Math.random().toString(36).substr(2, 9), |
| productId: p.id, |
| productLabel: `${p.brand} ${p.model} (${p.size})`, |
| dateTime: new Date().toISOString(), |
| type: 'STOCK_OUT', |
| quantity: item.quantity, |
| resultingStock, |
| adjustedBy: `${currentStaff.name} (${currentStaff.role})`, |
| reason: `Retail Invoice checkout sales: Ref ${newInv.invoiceNumber}` |
| }; |
| newLogs.push(logItem); |
|
|
| return { ...p, stock: resultingStock }; |
| } |
| return p; |
| }); |
| }); |
|
|
| const revisedHistory = [...newLogs, ...history]; |
| setHistory(revisedHistory); |
| setProducts(revisedProducts); |
|
|
| syncToLocalStorage('HBT_PRODUCTS', revisedProducts); |
| syncToLocalStorage('HBT_HISTORY', revisedHistory); |
| }; |
|
|
| |
| const handleAddStaffMember = (newUser: StaffUser) => { |
| const updated = [...staffList, newUser]; |
| setStaffList(updated); |
| syncToLocalStorage('HBT_STAFF', updated); |
| }; |
|
|
| const handleToggleStaffStatus = (staffId: string) => { |
| const updated = staffList.map(s => |
| s.id === staffId ? { ...s, active: !s.active } : s |
| ); |
| setStaffList(updated); |
| syncToLocalStorage('HBT_STAFF', updated); |
| }; |
|
|
| const handleUpdateStaffRole = (staffId: string, role: StaffRole) => { |
| const updated = staffList.map(s => |
| s.id === staffId ? { ...s, role } : s |
| ); |
| setStaffList(updated); |
| syncToLocalStorage('HBT_STAFF', updated); |
| }; |
|
|
| const handleSetCurrentStaff = (staff: StaffUser) => { |
| setCurrentStaff(staff); |
| syncToLocalStorage('HBT_CURRENT_STAFF', staff); |
| }; |
|
|
| |
| const handleRestoreDatabase = (decoded: { |
| settings: SystemSettings; |
| products: any[]; |
| invoices: any[]; |
| history: any[]; |
| staff: any[]; |
| }) => { |
| setSettings(decoded.settings); |
| setProducts(decoded.products); |
| setInvoices(decoded.invoices); |
| setHistory(decoded.history); |
| setStaffList(decoded.staff); |
|
|
| |
| syncToLocalStorage('HBT_SETTINGS', decoded.settings); |
| syncToLocalStorage('HBT_PRODUCTS', decoded.products); |
| syncToLocalStorage('HBT_INVOICES', decoded.invoices); |
| syncToLocalStorage('HBT_HISTORY', decoded.history); |
| syncToLocalStorage('HBT_STAFF', decoded.staff); |
|
|
| |
| const firstOwner = decoded.staff.find(s => s.role === 'owner' && s.active); |
| if (firstOwner) { |
| setCurrentStaff(firstOwner); |
| syncToLocalStorage('HBT_CURRENT_STAFF', firstOwner); |
| } |
| }; |
|
|
| |
| const handleClearCacheToDefault = () => { |
| localStorage.clear(); |
| }; |
|
|
| return ( |
| <div className="min-h-screen bg-[#F8FAFC] flex flex-col md:flex-row antialiased select-none font-sans" id="application-shell"> |
| {/* 1. COLLAPSIBLE OR RESPONSIVE STATIC SIDEBAR */} |
| <aside className="w-full md:w-64 bg-white border-b md:border-r border-slate-200 text-slate-800 flex flex-col justify-between shrink-0 shadow-sm" id="sidebar-panel"> |
| <div className="space-y-6"> |
| {/* Top Shop Brand Logo */} |
| <div className="p-6 border-b border-slate-100 flex items-center justify-between"> |
| <div className="flex items-center gap-3"> |
| <div className="w-9 h-9 rounded-lg bg-slate-900 flex items-center justify-center font-bold text-white text-base shadow-sm"> |
| HBT |
| </div> |
| <div className="space-y-0.5"> |
| <h2 className="text-xs font-bold tracking-tight text-slate-900 uppercase">Haider Brothers</h2> |
| <span className="text-[10px] text-slate-400 font-medium block">Tire Shop Manager</span> |
| </div> |
| </div> |
| </div> |
| |
| {/* Navigation Links list */} |
| <nav className="px-3 space-y-1"> |
| <button |
| onClick={() => setActiveTab('dashboard')} |
| className={`w-full flex items-center gap-3 px-3.5 py-2.5 rounded-lg text-xs font-medium font-sans transition cursor-pointer ${ |
| activeTab === 'dashboard' |
| ? 'bg-slate-900 text-white shadow-sm font-semibold' |
| : 'text-slate-500 hover:text-slate-900 hover:bg-slate-50' |
| }`} |
| > |
| <LayoutDashboard className="w-4 h-4 text-inherit" /> |
| Reporting Cockpit |
| </button> |
| |
| <button |
| onClick={() => { |
| setActiveTab('invoices'); |
| setSelectedInvoiceForView(null); |
| }} |
| className={`w-full flex items-center gap-3 px-3.5 py-2.5 rounded-lg text-xs font-medium font-sans transition cursor-pointer ${ |
| activeTab === 'invoices' |
| ? 'bg-slate-900 text-white shadow-sm font-semibold' |
| : 'text-slate-500 hover:text-slate-900 hover:bg-slate-50' |
| }`} |
| > |
| <ShoppingCart className="w-4 h-4 text-inherit" /> |
| Invoice Generator |
| </button> |
| |
| <button |
| onClick={() => setActiveTab('inventory')} |
| className={`w-full flex items-center gap-3 px-3.5 py-2.5 rounded-lg text-xs font-medium font-sans transition cursor-pointer ${ |
| activeTab === 'inventory' |
| ? 'bg-slate-900 text-white shadow-sm font-semibold' |
| : 'text-slate-500 hover:text-slate-900 hover:bg-slate-50' |
| }`} |
| > |
| <Package className="w-4 h-4 text-inherit" /> |
| Stock Inventory |
| </button> |
| |
| <button |
| onClick={() => setActiveTab('staff')} |
| className={`w-full flex items-center gap-3 px-3.5 py-2.5 rounded-lg text-xs font-medium font-sans transition cursor-pointer ${ |
| activeTab === 'staff' |
| ? 'bg-slate-900 text-white shadow-sm font-semibold' |
| : 'text-slate-500 hover:text-slate-900 hover:bg-slate-50' |
| }`} |
| > |
| <Users className="w-4 h-4 text-inherit" /> |
| Cashiers & RBAC |
| </button> |
| |
| <button |
| onClick={() => setActiveTab('social')} |
| className={`w-full flex items-center gap-3 px-3.5 py-2.5 rounded-lg text-xs font-medium font-sans transition cursor-pointer ${ |
| activeTab === 'social' |
| ? 'bg-slate-900 text-white shadow-sm font-semibold' |
| : 'text-slate-500 hover:text-slate-900 hover:bg-slate-50' |
| }`} |
| > |
| <Megaphone className="w-4 h-4 text-inherit" /> |
| Outreach Promos |
| </button> |
| |
| <button |
| onClick={() => setActiveTab('settings')} |
| className={`w-full flex items-center gap-3 px-3.5 py-2.5 rounded-lg text-xs font-medium font-sans transition cursor-pointer ${ |
| activeTab === 'settings' |
| ? 'bg-slate-900 text-white shadow-sm font-semibold' |
| : 'text-slate-500 hover:text-slate-900 hover:bg-slate-50' |
| }`} |
| > |
| <Settings className="w-4 h-4 text-inherit" /> |
| Settings & Vault |
| </button> |
| </nav> |
| </div> |
| |
| {/* Bottom Operator profile card in sidebar */} |
| <div className="p-4 border-t border-slate-100 bg-slate-50 space-y-2"> |
| <div className="flex items-center gap-2.5 text-xs"> |
| <UserCircle className="w-8 h-8 text-slate-400" /> |
| <div className="flex-1 min-w-0"> |
| <p className="font-semibold text-slate-950 truncate text-[11px]">{currentStaff.name}</p> |
| <p className="text-[10px] text-slate-400 uppercase tracking-wider font-medium">{currentStaff.role}</p> |
| </div> |
| </div> |
| |
| <div className="bg-white rounded-lg p-2 flex justify-between items-center text-[10px] border border-slate-200"> |
| <span className="text-slate-400 font-medium">Node secure:</span> |
| <span className="font-mono text-emerald-600 font-bold flex items-center gap-1.5"> |
| <span className="w-1.5 h-1.5 bg-emerald-500 rounded-full"></span> |
| ONLINE |
| </span> |
| </div> |
| </div> |
| </aside> |
| |
| {/* 2. CORE WORKSPACE: HEADER + SCROLLABLE PAGE VIEWS */} |
| <main className="flex-1 flex flex-col min-w-0 overflow-y-auto h-screen" id="workspace-viewport"> |
| {/* Top Navbar */} |
| <header className="bg-white border-b border-slate-200 px-6 md:px-8 py-4 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3 shrink-0"> |
| {/* Left info */} |
| <div className="flex items-center gap-2.5"> |
| <span className="w-2 h-2 bg-slate-900 rounded-full animate-pulse"></span> |
| <span className="text-[11px] font-bold text-slate-800 font-sans tracking-wider uppercase"> |
| SECURE CASHIER LEDGER |
| </span> |
| <span className="text-slate-200">|</span> |
| <span className="text-[11px] text-slate-500 flex items-center gap-1"> |
| Store Support: {settings.shopPhone} |
| </span> |
| </div> |
| |
| {/* Right systems specs */} |
| <div className="flex items-center gap-3.5 text-xs"> |
| {/* Real-time Clock */} |
| <div className="text-slate-600 flex items-center gap-1.5 bg-[#F8FAFC] px-2.5 py-1.5 rounded-lg border border-slate-200/80"> |
| <Clock className="w-3.5 h-3.5 text-slate-400" /> |
| <span className="font-mono font-medium text-[11px] text-slate-700">{currentTime.toLocaleTimeString()}</span> |
| </div> |
| |
| {/* Offline vs Online Beacons and alarms */} |
| {isOnline ? ( |
| <span className="bg-emerald-50 text-emerald-800 border border-emerald-200/60 font-semibold font-sans px-2.5 py-1.5 rounded-lg flex items-center gap-1.5 text-[10px]"> |
| <Wifi className="w-3.5 h-3.5 text-emerald-600" /> |
| VAULT OK |
| </span> |
| ) : ( |
| <span className="bg-amber-50 text-amber-800 border border-amber-200/60 font-semibold font-sans px-2.5 py-1.5 rounded-lg flex items-center gap-1.5 text-[10px] animate-pulse"> |
| <WifiOff className="w-3.5 h-3.5 text-amber-600" /> |
| AIR-GAPPED LOCAL |
| </span> |
| )} |
| </div> |
| </header> |
| |
| {/* Tab Pages dispatcher Router panel */} |
| <div className="flex-1 p-6 md:p-8 overflow-y-auto"> |
| <AnimatePresence mode="wait"> |
| <motion.div |
| key={activeTab} |
| initial={{ opacity: 0, y: 10 }} |
| animate={{ opacity: 1, y: 0 }} |
| exit={{ opacity: 0, y: -10 }} |
| transition={{ duration: 0.15 }} |
| className="h-full" |
| > |
| {activeTab === 'dashboard' && ( |
| <Dashboard |
| products={products} |
| invoices={invoices} |
| currentStaff={currentStaff} |
| settings={settings} |
| setActiveTab={setActiveTab} |
| onQuickAddStock={handleQuickAddStock} |
| setSelectedInvoiceForView={(inv) => { |
| setSelectedInvoiceForView(inv); |
| setActiveTab('invoices'); |
| }} |
| /> |
| )} |
| |
| {activeTab === 'invoices' && ( |
| <InvoiceGenerator |
| products={products} |
| invoices={invoices} |
| currentStaff={currentStaff} |
| settings={settings} |
| isOnline={isOnline} |
| onAddInvoice={handleAddInvoice} |
| selectedInvoiceForView={selectedInvoiceForView} |
| setSelectedInvoiceForView={setSelectedInvoiceForView} |
| /> |
| )} |
| |
| {activeTab === 'inventory' && ( |
| <InventoryManager |
| products={products} |
| history={history} |
| currentStaff={currentStaff} |
| settings={settings} |
| onAdjustProductStock={handleAdjustProductStock} |
| onAddNewProduct={handleAddNewProduct} |
| onDeleteProduct={handleDeleteProduct} |
| /> |
| )} |
| |
| {activeTab === 'staff' && ( |
| <StaffManager |
| staffList={staffList} |
| currentStaff={currentStaff} |
| onSetCurrentStaff={handleSetCurrentStaff} |
| onAddStaffMember={handleAddStaffMember} |
| onToggleStaffStatus={handleToggleStaffStatus} |
| onUpdateStaffRole={handleUpdateStaffRole} |
| /> |
| )} |
| |
| {activeTab === 'social' && ( |
| <SocialPromoter |
| products={products} |
| settings={settings} |
| /> |
| )} |
| |
| {activeTab === 'settings' && ( |
| <SettingsComponent |
| settings={settings} |
| isOnline={isOnline} |
| onUpdateSettings={handleUpdateSettings} |
| onToggleOnlineMode={() => setIsOnline(!isOnline)} |
| onRestoreDatabase={handleRestoreDatabase} |
| onClearCacheToDefault={handleClearCacheToDefault} |
| activeProducts={products} |
| activeInvoices={invoices} |
| activeHistory={history} |
| activeStaff={staffList} |
| /> |
| )} |
| </motion.div> |
| </AnimatePresence> |
| </div> |
| </main> |
| </div> |
| ); |
| } |
|
|