HBT-software / src /App.tsx
embedingHF's picture
Upload folder using huggingface_hub
46463e1 verified
Raw
History Blame Contribute Delete
21.2 kB
/**
* @license
* SPDX-License-Identifier: Apache-2.0
*/
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';
// Subcomponents
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());
// Core database state hooks
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]); // Zin Brother as default cashier
// Invoice review overlay trigger state
const [selectedInvoiceForView, setSelectedInvoiceForView] = useState<Invoice | null>(null);
// Initialize and load from local storage if existing
useEffect(() => {
// Clock tick
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);
// Ensure currentStaff matches the parsed currentStaff safely
setCurrentStaff(parsedCurrent);
} else {
setCurrentStaff(INITIAL_STAFF[1]); // Zain Brother manager
}
return () => clearInterval(timer);
}, []);
// Sync utilities to persistence
const syncToLocalStorage = (key: string, data: any) => {
localStorage.setItem(key, JSON.stringify(data));
};
// State Adjustments multipliers
const handleUpdateSettings = (updated: SystemSettings) => {
setSettings(updated);
syncToLocalStorage('HBT_SETTINGS', updated);
};
// Triggered from Dashboard replenishment alarms
const handleQuickAddStock = (productId: string, amount: number) => {
const adjustedProducts = products.map(p => {
if (p.id === productId) {
const resultingStock = p.stock + amount;
// Append history log immediately
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);
};
// Triggered from InventoryManager manual configurations
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;
// Create log item
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);
// Write initial STOCK_IN history entry
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);
};
// Adding Client Invoice automatically reduces matching tire quantities and writes logs
const handleAddInvoice = (newInv: Invoice) => {
// Append invoice
const updatedInvoices = [...invoices, newInv];
setInvoices(updatedInvoices);
syncToLocalStorage('HBT_INVOICES', updatedInvoices);
// Iterate items and decrease physical stock counts
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;
// Construct checkout history block
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);
};
// Staff managers
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);
};
// Decrypted Restore logic
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);
// Save and persist all entries
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);
// Set Owner as current operator safely if there's any owner in the backup list
const firstOwner = decoded.staff.find(s => s.role === 'owner' && s.active);
if (firstOwner) {
setCurrentStaff(firstOwner);
syncToLocalStorage('HBT_CURRENT_STAFF', firstOwner);
}
};
// Factory defaults
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 &amp; 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 &amp; 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>
);
}