import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { createRoot } from 'react-dom/client'; import jsPDF from 'jspdf'; import autoTable from 'jspdf-autotable'; // --- API UTILITY --- const API_BASE_URL = 'api'; async function apiCall(endpoint: string, method: 'GET' | 'POST' = 'POST', body?: any): Promise { const options: RequestInit = { method, headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, }; if (body && method !== 'GET') { options.body = JSON.stringify(body); } const response = await fetch(`${API_BASE_URL}/${endpoint}`, options); if (!response.ok) { const errorText = await response.text(); throw new Error(`API call failed for ${method} ${API_BASE_URL}/${endpoint}:\n${response.status} ${response.statusText}\n${errorText}`); } if (response.status === 204) { // Handle No Content success response return null as T; } return response.json(); } // --- TYPES & MOCKS --- type Page = 'dashboard' | 'products' | 'sales' | 'purchases' | 'orders' | 'payments' | 'expenses' | 'contacts' | 'returns' | 'damages' | 'reports' | 'users' | 'audit'; type Theme = 'light' | 'dark'; type Role = 'Admin' | 'Manager' | 'Staff'; type User = { id: number; name: string; email: string; role: Role; }; type Product = { id: number; sku: string; name: string; imageUrl?: string; description?: string; price: number; averageCost?: number; stock: number; }; type Contact = { id: number; name: string; email: string; role: 'Customer' | 'Vendor'; creditLimit?: number; }; type SaleItem = { productId: number; name: string; quantity: number; price: number; total: number; }; type Sale = { id: number; customerId: number; customerName: string; date: string; items: SaleItem[]; totalAmount: number; amountPaid: number; balance: number; status: 'Paid' | 'Partially Paid' | 'Unpaid'; }; type PurchaseItem = { productId: number; name: string; quantity: number; cost: number; total: number; }; type Purchase = { id: number; vendorId: number; vendorName: string; date: string; items: PurchaseItem[]; totalAmount: number; amountPaid: number; balance: number; status: 'Paid' | 'Partially Paid' | 'Unpaid'; }; type Order = { id: number; customerId: number; customerName: string; date: string; items: SaleItem[]; totalAmount: number; status: 'Received' | 'Processed' | 'Delivered'; }; type ExpenseCategory = { id: number; name: string; }; type Expense = { id: number; date: string; description: string; categoryId: number; categoryName: string; amount: number; }; type Payment = { id: number; date: string; type: 'Sale' | 'Purchase'; transactionId: number; contactName: string; amount: number; method: 'Cash' | 'Bank Transfer' | 'Credit Card' | 'Mobile Wallet'; reference: string; }; type ReturnItem = { productId: number; name: string; quantity: number; price: number; total: number; }; type Return = { id: number; date: string; saleId: number; customerName: string; items: ReturnItem[]; totalValue: number; }; type Damage = { id: number; date: string; productId: number; productName: string; quantity: number; reason: string; }; type AuditLogEntry = { id: string; timestamp: string; userId: number; userName: string; action: string; details: string; }; const navItems: { id: Page; label: string; icon: string }[] = [ { id: 'dashboard', label: 'Dashboard', icon: 'dashboard' }, { id: 'products', label: 'Products', icon: 'inventory_2' }, { id: 'sales', label: 'Sales', icon: 'receipt_long' }, { id: 'purchases', label: 'Purchases', icon: 'shopping_cart' }, { id: 'orders', label: 'Orders', icon: 'list_alt' }, { id: 'returns', label: 'Returns', icon: 'assignment_return' }, { id: 'damages', label: 'Damaged Stock', icon: 'report' }, { id: 'payments', label: 'Payments', icon: 'paid' }, { id: 'expenses', label: 'Expenses', icon: 'wallet' }, { id: 'contacts', label: 'Contacts', icon: 'contacts' }, { id: 'reports', label: 'Reports', icon: 'analytics' }, { id: 'users', label: 'Users', icon: 'group' }, { id: 'audit', label: 'Audit Trail', icon: 'history' }, ]; const permissions = { canViewPage: (role: Role, page: Page) => { if (role === 'Admin') return true; if (role === 'Manager') { return !['users', 'audit'].includes(page); } if (role === 'Staff') { const allowedPages: Page[] = ['dashboard', 'sales', 'purchases', 'orders', 'payments', 'expenses', 'contacts', 'returns', 'damages']; return allowedPages.includes(page); } return false; }, canManageProducts: (role: Role) => { return role === 'Admin' || role === 'Manager'; }, canAddEditDeleteProducts: (role: Role) => { return role === 'Admin' || role === 'Manager'; } }; const formatCurrency = (amount: number) => { return `Rs. ${amount.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; }; const mockStats = [ { title: "Total Revenue", value: formatCurrency(54320), change: "+5.2%", icon: "monitoring", changeType: "positive" }, { title: "Today's Sales", value: formatCurrency(1890), change: "+12.1%", icon: "today", changeType: "positive" }, { title: "Pending Orders", value: "24", change: "-2.5%", icon: "pending_actions", changeType: "negative" }, { title: "New Customers", value: "12", change: "+8.0%", icon: "person_add", changeType: "positive" }, ]; // --- UTILITIES --- const downloadAsCsv = (filename: string, headers: string[], data: (string | number)[][]) => { const csvContent = [ headers.join(','), ...data.map(row => row.map(field => `"${String(field).replace(/"/g, '""')}"`).join(',')) ].join('\n'); const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); if (link.href) { URL.revokeObjectURL(link.href); } link.href = URL.createObjectURL(blob); link.download = `${filename}.csv`; link.style.visibility = 'hidden'; document.body.appendChild(link); link.click(); document.body.removeChild(link); }; const downloadAsPdf = (filename: string, title: string, headers: {header: string, dataKey: string}[], data: Record[]) => { const doc = new jsPDF(); doc.text(title, 14, 16); autoTable(doc, { head: [headers.map(h => h.header)], body: data.map(row => headers.map(h => row[h.dataKey])), startY: 22, }); doc.save(`${filename}.pdf`); }; // --- REUSABLE COMPONENTS --- const Modal: React.FC<{ isOpen: boolean; onClose: () => void; title: string; children: React.ReactNode; footer?: React.ReactNode; size?: 'sm' | 'md' | 'lg'; }> = ({ isOpen, onClose, title, children, footer, size = 'md' }) => { const [isRendered, setIsRendered] = useState(isOpen); const modalRef = useRef(null); const triggerElementRef = useRef(null); useEffect(() => { if (isOpen) { triggerElementRef.current = document.activeElement as HTMLElement; setIsRendered(true); } }, [isOpen]); const onAnimationEnd = () => { if (!isOpen) { setIsRendered(false); triggerElementRef.current?.focus(); } }; useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape' && isOpen) { onClose(); } if (event.key === 'Tab' && isOpen && modalRef.current) { const focusableElements = modalRef.current.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' ); if (focusableElements.length === 0) return; const firstElement = focusableElements[0]; const lastElement = focusableElements[focusableElements.length - 1]; if (event.shiftKey) { if (document.activeElement === firstElement) { lastElement.focus(); event.preventDefault(); } } else { if (document.activeElement === lastElement) { firstElement.focus(); event.preventDefault(); } } } }; document.addEventListener('keydown', handleKeyDown); if (isOpen && isRendered) { setTimeout(() => { modalRef.current?.querySelector( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' )?.focus(); }, 50); } return () => { document.removeEventListener('keydown', handleKeyDown); }; }, [isOpen, isRendered, onClose]); if (!isRendered) return null; return (
e.stopPropagation()}>
{children}
{footer &&
{footer}
}
); }; const ConfirmationModal: React.FC<{ isOpen: boolean; onClose: () => void; onConfirm: () => void; title: string; message: string; isSaving?: boolean; }> = ({ isOpen, onClose, onConfirm, title, message, isSaving = false }) => ( } >

{message}

); const Pagination: React.FC<{ currentPage: number; totalItems: number; itemsPerPage: number; onPageChange: (page: number) => void; }> = ({ currentPage, totalItems, itemsPerPage, onPageChange }) => { const totalPages = Math.ceil(totalItems / itemsPerPage); if (totalPages <= 1) return null; const handlePageClick = (page: number) => { if (page >= 1 && page <= totalPages) { onPageChange(page); } }; const pageNumbers = []; for (let i = 1; i <= totalPages; i++) { pageNumbers.push(i); } return (
{pageNumbers.map(number => ( ))}
); }; // --- AI INSIGHTS COMPONENT --- const AIInsights: React.FC = () => { const [prompt, setPrompt] = useState(''); const [loading, setLoading] = useState(false); const [result, setResult] = useState(''); const handleGenerate = async () => { if (!prompt.trim()) return; setLoading(true); setResult(''); try { const businessContext = ` You are a business analyst for Zargoona Wholesale Management. Here is a snapshot of our current business data: - Total Revenue: ${formatCurrency(54320)} - Sales Today: ${formatCurrency(1890)} - Top Selling Product: 'Product A' (500 units sold this month) - Slowest Selling Product: 'Product C' (20 units sold this month) - Pending Orders: 24 - New Customers this month: 45 Please provide a concise, actionable insight based on the user's question. `; const fullPrompt = `${businessContext}\n\nUser Question: "${prompt}"`; // Call our secure PHP backend script instead of the Gemini SDK directly const response = await apiCall<{ text: string }>('ai_handler.php', 'POST', { prompt: fullPrompt }); setResult(response.text); } catch (error) { console.error("AI generation failed:", error); setResult("A server error occurred while generating the insight. Please ensure 'api/ai_handler.php' is configured correctly."); } finally { setLoading(false); } }; return (

AI-Powered Insights

setPrompt(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleGenerate()} disabled={loading} aria-label="Ask for an AI insight" />
{(loading || result) && (
{loading ? ( <>
Generating insight... ) : ( result )}
)}
); }; // --- DASHBOARD PAGE --- const DashboardPage: React.FC = () => { return ( <>

Dashboard

{mockStats.map((stat) => (
{stat.title} {stat.icon}
{stat.value}
{stat.change} vs last period
))}
); }; // --- PROFILE MODAL --- const ProfileModal: React.FC<{ isOpen: boolean; onClose: () => void; user: User; onSave: (user: Omit) => void; }> = ({ isOpen, onClose, user, onSave }) => { const [name, setName] = useState(user.name); const [currentPassword, setCurrentPassword] = useState(''); const [newPassword, setNewPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); const [isSaving, setIsSaving] = useState(false); useEffect(() => { if (isOpen) { setName(user.name); setCurrentPassword(''); setNewPassword(''); setConfirmPassword(''); setIsSaving(false); } }, [isOpen, user]); const handleSave = async () => { setIsSaving(true); try { const payload: any = { id: user.id }; if (name !== user.name) payload.name = name; if (newPassword) { if (newPassword !== confirmPassword) { alert("New passwords do not match."); setIsSaving(false); return; } payload.currentPassword = currentPassword; payload.newPassword = newPassword; } await apiCall('users.php', 'POST', { action: 'updateProfile', data: payload }); onSave({ name }); } catch (error) { console.error("Failed to update profile", error); alert(`Error updating profile: ${(error as Error).message}`); } finally { setIsSaving(false); } }; return ( } >

Personal Information

setName(e.target.value)} />

Change Password

setCurrentPassword(e.target.value)} placeholder="Required to change password" />
setNewPassword(e.target.value)} placeholder="Leave blank to keep unchanged" />
setConfirmPassword(e.target.value)} />
); }; // --- PRODUCTS PAGE --- const CameraModal: React.FC<{ isOpen: boolean; onClose: () => void; onCapture: (dataUrl: string) => void; }> = ({ isOpen, onClose, onCapture }) => { const videoRef = useRef(null); const canvasRef = useRef(null); const streamRef = useRef(null); const [error, setError] = useState(null); const stopStream = useCallback(() => { if (streamRef.current) { streamRef.current.getTracks().forEach(track => track.stop()); streamRef.current = null; } }, []); useEffect(() => { if (isOpen) { setError(null); navigator.mediaDevices.getUserMedia({ video: true }) .then(stream => { streamRef.current = stream; if (videoRef.current) { videoRef.current.srcObject = stream; } }) .catch(err => { console.error("Camera access denied:", err); setError("Camera access was denied. Please allow camera permissions in your browser settings."); }); } else { stopStream(); } return () => { stopStream(); }; }, [isOpen, stopStream]); const handleCapture = () => { const video = videoRef.current; const canvas = canvasRef.current; if (video && canvas) { canvas.width = video.videoWidth; canvas.height = video.videoHeight; const context = canvas.getContext('2d'); context?.drawImage(video, 0, 0, canvas.width, canvas.height); const dataUrl = canvas.toDataURL('image/png'); onCapture(dataUrl); onClose(); } }; return ( {error ? (

{error}

) : (
)}
); }; const ProductModal: React.FC<{ isOpen: boolean; onClose: () => void; onSave: (product: Product) => void; productToEdit: Product | null; }> = ({ isOpen, onClose, onSave, productToEdit }) => { const [product, setProduct] = useState>({ sku: '', name: '', price: 0, imageUrl: '', description: '', averageCost: 0, stock: 0 }); const [isSaving, setIsSaving] = useState(false); const [isCameraOpen, setIsCameraOpen] = useState(false); const fileInputRef = useRef(null); useEffect(() => { if (isOpen) { setIsSaving(false); if (productToEdit) { setProduct(productToEdit); } else { setProduct({ sku: '', name: '', price: 0, imageUrl: '', description: '', averageCost: 0, stock: 0 }); } } }, [isOpen, productToEdit]); const handleChange = (e: React.ChangeEvent) => { const { name, value, type } = e.target; setProduct(prev => ({ ...prev, [name]: type === 'number' ? parseFloat(value) || 0 : value })); }; const handleFileSelected = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (file) { const reader = new FileReader(); reader.onload = (e) => { const dataUrl = e.target?.result as string; setProduct(prev => ({ ...prev, imageUrl: dataUrl })); }; reader.readAsDataURL(file); } }; const handleCapture = (dataUrl: string) => { setProduct(prev => ({ ...prev, imageUrl: dataUrl })); setIsCameraOpen(false); }; const handleRemoveImage = () => { setProduct(prev => ({ ...prev, imageUrl: '' })); if(fileInputRef.current) { fileInputRef.current.value = ''; } }; const handleSave = async () => { setIsSaving(true); try { const payload = { ...product, ...(productToEdit && { id: productToEdit.id }), }; const action = productToEdit ? 'update' : 'create'; const savedProduct = await apiCall('products.php', 'POST', { action, data: payload }); onSave(savedProduct); } catch (error) { console.error("Failed to save product", error); alert(`Error saving product: ${(error as Error).message}`); } finally { setIsSaving(false); } }; return ( <> } >
{product.imageUrl ? ( Product Preview ) : (
image No Image
)}
{product.imageUrl && ( )}

Stock Level

{!!productToEdit && Stock levels must be updated via Purchases, Returns, or Damages.}
setIsCameraOpen(false)} onCapture={handleCapture} /> ); }; const BulkPriceUpdateModal: React.FC<{ isOpen: boolean; onClose: () => void; onSave: (type: 'fixed' | 'increase' | 'decrease', value: number, isPercentage: boolean) => void; selectedCount: number; }> = ({ isOpen, onClose, onSave, selectedCount }) => { const [updateType, setUpdateType] = useState<'fixed' | 'increase' | 'decrease'>('fixed'); const [value, setValue] = useState(0); const [isPercentage, setIsPercentage] = useState(false); const [isSaving, setIsSaving] = useState(false); const handleSave = async () => { setIsSaving(true); await new Promise(res => setTimeout(res, 200)); // simulate async onSave(updateType, value, isPercentage); setIsSaving(false); }; return ( } >
setValue(parseFloat(e.target.value) || 0)} min="0" />
{updateType !== 'fixed' && (
)}
); }; const ProductsPage: React.FC<{ products: Product[]; setProducts: React.Dispatch>; currentUser: User | null; }> = ({ products, setProducts, currentUser }) => { const [isModalOpen, setIsModalOpen] = useState(false); const [isConfirmOpen, setIsConfirmOpen] = useState(false); const [isBulkPriceModalOpen, setIsBulkPriceModalOpen] = useState(false); const [editingProduct, setEditingProduct] = useState(null); const [productToDelete, setProductToDelete] = useState(null); const [searchTerm, setSearchTerm] = useState(''); const [stockStatusFilter, setStockStatusFilter] = useState<'all' | 'inStock' | 'lowStock' | 'outOfStock'>('all'); const [currentPage, setCurrentPage] = useState(1); const [selectedProductIds, setSelectedProductIds] = useState>(new Set()); const [isDeleting, setIsDeleting] = useState(false); const fileInputRef = useRef(null); const itemsPerPage = 10; const canManageProducts = currentUser && permissions.canAddEditDeleteProducts(currentUser.role); const filteredProducts = useMemo(() => { return products.filter(p => { // Stock Status Filter if (stockStatusFilter !== 'all') { const totalStock = p.stock; if (stockStatusFilter === 'outOfStock' && totalStock !== 0) { return false; } if (stockStatusFilter === 'lowStock' && (totalStock === 0 || totalStock >= 20)) { return false; } if (stockStatusFilter === 'inStock' && totalStock < 20) { return false; } } // Text Search Filter const searchKeywords = searchTerm.toLowerCase().split(' ').filter(k => k); if (searchKeywords.length === 0) { return true; } const searchableText = `${p.name.toLowerCase()} ${p.sku.toLowerCase()} ${p.description?.toLowerCase() || ''}`; return searchKeywords.every(keyword => searchableText.includes(keyword)); }); }, [products, searchTerm, stockStatusFilter]); const paginatedProducts = useMemo(() => { const startIndex = (currentPage - 1) * itemsPerPage; return filteredProducts.slice(startIndex, startIndex + itemsPerPage); }, [filteredProducts, currentPage, itemsPerPage]); useEffect(() => { setCurrentPage(1); }, [searchTerm, stockStatusFilter]); useEffect(() => { setSelectedProductIds(new Set()); }, [searchTerm, products, stockStatusFilter]); const getStockStatus = (stock: number) => { if (stock === 0) return { text: 'Out of Stock', className: 'out-of-stock' }; if (stock < 20) return { text: 'Low Stock', className: 'low-stock' }; return { text: 'In Stock', className: 'in-stock' }; }; const handleOpenAddModal = () => { setEditingProduct(null); setIsModalOpen(true); }; const handleOpenEditModal = (product: Product) => { setEditingProduct(product); setIsModalOpen(true); }; const handleOpenDeleteConfirm = (product: Product) => { setProductToDelete(product); setIsConfirmOpen(true); }; const handleCloseModals = () => { setIsModalOpen(false); setIsConfirmOpen(false); setIsBulkPriceModalOpen(false); setEditingProduct(null); setProductToDelete(null); }; const handleSaveProduct = (savedProduct: Product) => { if (editingProduct) { // Editing setProducts(products.map(p => p.id === savedProduct.id ? savedProduct : p)); } else { // Adding setProducts([savedProduct, ...products]); } handleCloseModals(); }; const handleConfirmDelete = async () => { setIsDeleting(true); try { if (productToDelete) { await apiCall('products.php', 'POST', { action: 'delete', data: { id: productToDelete.id }}); setProducts(products.filter(p => p.id !== productToDelete.id)); } else if (selectedProductIds.size > 0) { await apiCall('products.php', 'POST', { action: 'bulkDelete', data: { ids: Array.from(selectedProductIds) }}); setProducts(products.filter(p => !selectedProductIds.has(p.id))); setSelectedProductIds(new Set()); } } catch (error) { console.error("Failed to delete product(s)", error); alert(`Error deleting: ${(error as Error).message}`); } finally { setIsDeleting(false); handleCloseModals(); } }; const handleToggleSelect = (productId: number) => { const newSelection = new Set(selectedProductIds); if (newSelection.has(productId)) { newSelection.delete(productId); } else { newSelection.add(productId); } setSelectedProductIds(newSelection); }; const handleToggleSelectAll = () => { const allVisibleIds = new Set(paginatedProducts.map(p => p.id)); if (selectedProductIds.size === allVisibleIds.size && paginatedProducts.length > 0) { setSelectedProductIds(new Set()); } else { setSelectedProductIds(allVisibleIds); } }; const handleExportTemplate = () => { const headers = ['sku', 'name', 'description', 'imageUrl', 'price', 'stock']; downloadAsCsv('product-import-template', headers, []); }; const handleImportClick = () => { fileInputRef.current?.click(); }; const handleFileChange = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = async (e) => { try { const text = e.target?.result as string; // This entire block should be a single API call const response = await apiCall<{ importedProducts: Product[] }>('products.php', 'POST', { action: 'import', data: { csv: text } }); setProducts(prev => [...response.importedProducts, ...prev]); alert(`${response.importedProducts.length} products imported successfully!`); } catch (error) { console.error("CSV import failed:", error); alert(`CSV import failed: ${(error as Error).message}`); } }; reader.readAsText(file); event.target.value = ''; // Reset file input }; const handleSaveBulkPriceUpdate = async (type: 'fixed' | 'increase' | 'decrease', value: number, isPercentage: boolean) => { try { const payload = { ids: Array.from(selectedProductIds), update: { type, value, isPercentage } }; const updatedProducts = await apiCall('products.php', 'POST', { action: 'bulkPriceUpdate', data: payload }); const updatedProductsMap = new Map(updatedProducts.map(p => [p.id, p])); setProducts(prev => prev.map(p => updatedProductsMap.get(p.id) || p)); setSelectedProductIds(new Set()); setIsBulkPriceModalOpen(false); } catch (error) { console.error("Failed to update prices", error); alert(`Error updating prices: ${(error as Error).message}`); } }; return ( <>

Products

{canManageProducts && (
)}
search setSearchTerm(e.target.value)} />
{searchTerm && (
Filtering by: "{searchTerm}"
)}
{canManageProducts && ( )} {canManageProducts && } {paginatedProducts.map((product) => { const status = getStockStatus(product.stock); return ( {canManageProducts && ( )} {canManageProducts && ( )} ); })}
0 && paginatedProducts.every(p => selectedProductIds.has(p.id))} onChange={handleToggleSelectAll} /> Image SKU Product Name Description Price Total Stock StatusActions
handleToggleSelect(product.id)} /> {product.name} e.currentTarget.src = 'https://via.placeholder.com/50'} /> {product.sku} {product.name} {product.description} {formatCurrency(product.price)} {product.stock} {status.text}
{selectedProductIds.size > 0 && canManageProducts && (
{selectedProductIds.size} selected
)} {canManageProducts && ( <> )} ); }; // --- SALES PAGE --- const SaleModal: React.FC<{ isOpen: boolean; onClose: () => void; onSave: (sale: Sale, updatedProducts: Product[]) => void; products: Product[]; contacts: Contact[]; saleToEdit: Sale | null; }> = ({ isOpen, onClose, onSave, products, contacts, saleToEdit }) => { const [customerId, setCustomerId] = useState(''); const [items, setItems] = useState([]); const [amountPaid, setAmountPaid] = useState(''); const [currentProductId, setCurrentProductId] = useState(''); const [currentQuantity, setCurrentQuantity] = useState(1); const [isSaving, setIsSaving] = useState(false); const customers = contacts.filter(c => c.role === 'Customer'); useEffect(() => { if (isOpen) { setIsSaving(false); if (saleToEdit) { setCustomerId(saleToEdit.customerId); setItems(saleToEdit.items); setAmountPaid(saleToEdit.amountPaid); } else { setCustomerId(''); setItems([]); setAmountPaid(''); } setCurrentProductId(''); setCurrentQuantity(1); } }, [isOpen, saleToEdit]); const selectedProduct = products.find(p => p.id === currentProductId); const availableStock = useMemo(() => { if (!selectedProduct) return 0; const originalItem = saleToEdit?.items.find(item => item.productId === selectedProduct.id); const originalQuantity = originalItem ? originalItem.quantity : 0; return selectedProduct.stock + originalQuantity; }, [selectedProduct, saleToEdit]); const handleAddItem = () => { if (!currentProductId || !currentQuantity || !selectedProduct) return; const newQuantity = Number(currentQuantity); const existingItemIndex = items.findIndex(item => item.productId === currentProductId); let newItems = [...items]; if (existingItemIndex > -1) { newItems[existingItemIndex].quantity += newQuantity; newItems[existingItemIndex].total = newItems[existingItemIndex].quantity * newItems[existingItemIndex].price; } else { newItems.push({ productId: selectedProduct.id, name: selectedProduct.name, quantity: newQuantity, price: selectedProduct.price, total: newQuantity * selectedProduct.price, }); } setItems(newItems); setCurrentProductId(''); setCurrentQuantity(1); }; const handleRemoveItem = (productId: number) => { setItems(items.filter(item => item.productId !== productId)); }; const totalAmount = items.reduce((sum, item) => sum + item.total, 0); const paid = Number(amountPaid) || 0; const handleSave = async () => { if (!customerId || items.length === 0) return; setIsSaving(true); try { const payload = { customerId, items: items.map(({ name, total, ...item }) => item), // Sanitize for API amountPaid: paid, ...(saleToEdit && { id: saleToEdit.id }), }; const action = saleToEdit ? 'update' : 'create'; const response = await apiCall<{ savedSale: Sale; updatedProducts: Product[] }>('sales.php', 'POST', { action, data: payload }); onSave(response.savedSale, response.updatedProducts); } catch (error) { console.error("Failed to save sale", error); alert(`Error saving sale: ${(error as Error).message}`); } finally { setIsSaving(false); } }; return ( } >

Add Products

setCurrentQuantity(Number(e.target.value))} disabled={!currentProductId}/>
{items.length > 0 && ( {items.map(item => ( ))}
ProductQtyPriceTotal
{item.name} {item.quantity} {formatCurrency(item.price)} {formatCurrency(item.total)}
)}
setAmountPaid(Number(e.target.value) >= 0 ? Number(e.target.value) : '')} />

Total: {formatCurrency(totalAmount)}

Balance: {formatCurrency(totalAmount - paid)}

); }; const SalesPage: React.FC<{ sales: Sale[], products: Product[], contacts: Contact[], setSales: React.Dispatch>, setProducts: React.Dispatch>; }> = ({ sales, products, contacts, setSales, setProducts }) => { const [isModalOpen, setIsModalOpen] = useState(false); const [editingSale, setEditingSale] = useState(null); const [searchTerm, setSearchTerm] = useState(''); const [currentPage, setCurrentPage] = useState(1); const [startDate, setStartDate] = useState(''); const [endDate, setEndDate] = useState(''); const [selectedCustomer, setSelectedCustomer] = useState(''); const [selectedStatus, setSelectedStatus] = useState<'' | Sale['status']>(''); const itemsPerPage = 10; const customers = useMemo(() => contacts.filter(c => c.role === 'Customer'), [contacts]); const filteredSales = useMemo(() => { const searchKeywords = searchTerm.toLowerCase().split(' ').filter(k => k); return sales.filter(s => { const searchableText = `${s.customerName.toLowerCase()} #${s.id} ${s.status.toLowerCase()}`; if (searchKeywords.length > 0 && !searchKeywords.every(keyword => searchableText.includes(keyword))) { return false; } if (startDate && s.date < startDate) return false; if (endDate && s.date > endDate) return false; if (selectedCustomer && s.customerId !== selectedCustomer) return false; if (selectedStatus && s.status !== selectedStatus) return false; return true; }); }, [sales, searchTerm, startDate, endDate, selectedCustomer, selectedStatus]); const paginatedSales = useMemo(() => { const startIndex = (currentPage - 1) * itemsPerPage; return filteredSales.slice(startIndex, startIndex + itemsPerPage); }, [filteredSales, currentPage, itemsPerPage]); useEffect(() => { setCurrentPage(1); }, [searchTerm, startDate, endDate, selectedCustomer, selectedStatus]); const handleOpenAddModal = () => { setEditingSale(null); setIsModalOpen(true); }; const handleOpenEditModal = (sale: Sale) => { setEditingSale(sale); setIsModalOpen(true); }; const handleSaveSale = (savedSale: Sale, updatedProducts: Product[]) => { const updatedProductsMap = new Map(updatedProducts.map(p => [p.id, p])); setProducts(prev => prev.map(p => updatedProductsMap.get(p.id) || p)); if (editingSale) { // Update existing sale setSales(sales.map(s => s.id === savedSale.id ? savedSale : s)); } else { // Create new sale setSales(prev => [savedSale, ...prev]); } setIsModalOpen(false); }; const getStatusClass = (status: Sale['status']) => { if (status === 'Paid') return 'status-paid'; if (status === 'Partially Paid') return 'status-partial'; if (status === 'Unpaid') return 'status-unpaid'; return ''; }; const handleExport = (format: 'csv' | 'pdf') => { const dataToExport = filteredSales; if (dataToExport.length === 0) { alert("No data to export for the current filters."); return; } const filename = `sales-report-${new Date().toISOString().split('T')[0]}`; if (format === 'csv') { const headers = ['Invoice ID', 'Customer', 'Date', 'Total Amount', 'Amount Paid', 'Balance', 'Status']; const data = dataToExport.map(s => [s.id, s.customerName, s.date, s.totalAmount, s.amountPaid, s.balance, s.status]); downloadAsCsv(filename, headers, data); } else if (format === 'pdf') { const headers = [ { header: 'Invoice ID', dataKey: 'id' }, { header: 'Customer', dataKey: 'customerName' }, { header: 'Date', dataKey: 'date' }, { header: 'Total', dataKey: 'totalAmount' }, { header: 'Paid', dataKey: 'amountPaid' }, { header: 'Balance', dataKey: 'balance' }, { header: 'Status', dataKey: 'status' }, ]; const data = dataToExport.map(s => ({...s, totalAmount: formatCurrency(s.totalAmount), amountPaid: formatCurrency(s.amountPaid), balance: formatCurrency(s.balance)})); downloadAsPdf(filename, `Sales Report`, headers, data); } }; const clearFilters = () => { setSearchTerm(''); setStartDate(''); setEndDate(''); setSelectedCustomer(''); setSelectedStatus(''); }; return ( <>

Sales

search setSearchTerm(e.target.value)} />
setStartDate(e.target.value)} />
setEndDate(e.target.value)} />
{paginatedSales.map((sale) => ( ))}
Invoice ID Customer Date Total Amount Paid Balance Status Actions
#{sale.id} {sale.customerName} {sale.date} {formatCurrency(sale.totalAmount)} {formatCurrency(sale.amountPaid)} {formatCurrency(sale.balance)} {sale.status}
setIsModalOpen(false)} onSave={handleSaveSale} products={products} contacts={contacts} saleToEdit={editingSale} /> ); }; // --- PURCHASES PAGE --- const AddPurchaseModal: React.FC<{ isOpen: boolean; onClose: () => void; onSave: (purchase: Purchase, updatedProducts: Product[]) => void; products: Product[]; contacts: Contact[]; }> = ({ isOpen, onClose, onSave, products, contacts }) => { const [vendorId, setVendorId] = useState(''); const [items, setItems] = useState[]>([]); const [amountPaid, setAmountPaid] = useState(''); const [currentProductId, setCurrentProductId] = useState(''); const [currentQuantity, setCurrentQuantity] = useState(1); const [currentCost, setCurrentCost] = useState(''); const [isSaving, setIsSaving] = useState(false); const vendors = contacts.filter(c => c.role === 'Vendor'); useEffect(() => { if (isOpen) { setIsSaving(false); setVendorId(''); setItems([]); setAmountPaid(''); setCurrentProductId(''); setCurrentQuantity(1); setCurrentCost(''); } }, [isOpen]); const selectedProduct = products.find(p => p.id === currentProductId); const handleAddItem = () => { if (!currentProductId || !currentQuantity || !currentCost || !selectedProduct) return; const newQuantity = Number(currentQuantity); const newCost = Number(currentCost); setItems(prev => [ ...prev, { productId: selectedProduct.id, quantity: newQuantity, cost: newCost, } ]); setCurrentProductId(''); setCurrentQuantity(1); setCurrentCost(''); }; const handleRemoveItem = (index: number) => { setItems(prev => prev.filter((_, i) => i !== index)); }; const itemsWithDetails = items.map(item => { const product = products.find(p => p.id === item.productId); return { ...item, name: product?.name || 'Unknown', total: item.quantity * item.cost, } }); const totalAmount = itemsWithDetails.reduce((sum, item) => sum + item.total, 0); const paid = Number(amountPaid) || 0; const handleSave = async () => { if (!vendorId || items.length === 0) return; setIsSaving(true); try { const payload = { vendorId, items, amountPaid: paid }; const response = await apiCall<{ savedPurchase: Purchase; updatedProducts: Product[] }>('purchases.php', 'POST', { action: 'create', data: payload }); onSave(response.savedPurchase, response.updatedProducts); } catch (error) { console.error("Failed to save purchase", error); alert(`Error saving purchase: ${(error as Error).message}`); } finally { setIsSaving(false); } }; return ( } >

Add Products

setCurrentQuantity(Number(e.target.value))} disabled={!currentProductId}/>
setCurrentCost(Number(e.target.value))} disabled={!currentProductId}/>
{itemsWithDetails.length > 0 && ( {itemsWithDetails.map((item, index) => ( ))}
ProductQtyCostTotal
{item.name} {item.quantity} {formatCurrency(item.cost)} {formatCurrency(item.total)}
)}
setAmountPaid(Number(e.target.value) >= 0 ? Number(e.target.value) : '')} />

Total: {formatCurrency(totalAmount)}

Balance: {formatCurrency(totalAmount - paid)}

); }; const PurchasesPage: React.FC<{ purchases: Purchase[], products: Product[], contacts: Contact[], setPurchases: React.Dispatch>, setProducts: React.Dispatch>; }> = ({ purchases, products, contacts, setPurchases, setProducts }) => { const [isModalOpen, setIsModalOpen] = useState(false); const [searchTerm, setSearchTerm] = useState(''); const [currentPage, setCurrentPage] = useState(1); const [startDate, setStartDate] = useState(''); const [endDate, setEndDate] = useState(''); const [selectedVendor, setSelectedVendor] = useState(''); const [selectedStatus, setSelectedStatus] = useState<'' | Purchase['status']>(''); const itemsPerPage = 10; const vendors = useMemo(() => contacts.filter(c => c.role === 'Vendor'), [contacts]); const filteredPurchases = useMemo(() => { const searchKeywords = searchTerm.toLowerCase().split(' ').filter(k => k); return purchases.filter(p => { const searchableText = `${p.vendorName.toLowerCase()} #${p.id} ${p.status.toLowerCase()}`; if (searchKeywords.length > 0 && !searchKeywords.every(keyword => searchableText.includes(keyword))) { return false; } if (startDate && p.date < startDate) return false; if (endDate && p.date > endDate) return false; if (selectedVendor && p.vendorId !== selectedVendor) return false; if (selectedStatus && p.status !== selectedStatus) return false; return true; }); }, [purchases, searchTerm, startDate, endDate, selectedVendor, selectedStatus]); const paginatedPurchases = useMemo(() => { const startIndex = (currentPage - 1) * itemsPerPage; return filteredPurchases.slice(startIndex, startIndex + itemsPerPage); }, [filteredPurchases, currentPage, itemsPerPage]); useEffect(() => { setCurrentPage(1); }, [searchTerm, startDate, endDate, selectedVendor, selectedStatus]); const handleSavePurchase = (newPurchase: Purchase, updatedProducts: Product[]) => { setPurchases(prev => [newPurchase, ...prev]); const updatedProductsMap = new Map(updatedProducts.map(p => [p.id, p])); setProducts(prev => prev.map(p => updatedProductsMap.get(p.id) || p)); setIsModalOpen(false); }; const getStatusClass = (status: Purchase['status']) => { if (status === 'Paid') return 'status-paid'; if (status === 'Partially Paid') return 'status-partial'; if (status === 'Unpaid') return 'status-unpaid'; return ''; }; const handleExport = (format: 'csv' | 'pdf') => { const dataToExport = filteredPurchases; if (dataToExport.length === 0) { alert("No data to export for the current filters."); return; } const filename = `purchases-report-${new Date().toISOString().split('T')[0]}`; if (format === 'csv') { const headers = ['Order ID', 'Vendor', 'Date', 'Total Amount', 'Amount Paid', 'Balance', 'Status']; const data = dataToExport.map(p => [p.id, p.vendorName, p.date, p.totalAmount, p.amountPaid, p.balance, p.status]); downloadAsCsv(filename, headers, data); } else if (format === 'pdf') { const headers = [ { header: 'Order ID', dataKey: 'id' }, { header: 'Vendor', dataKey: 'vendorName' }, { header: 'Date', dataKey: 'date' }, { header: 'Total', dataKey: 'totalAmount' }, { header: 'Paid', dataKey: 'amountPaid' }, { header: 'Balance', dataKey: 'balance' }, { header: 'Status', dataKey: 'status' }, ]; const data = dataToExport.map(s => ({...s, totalAmount: formatCurrency(s.totalAmount), amountPaid: formatCurrency(s.amountPaid), balance: formatCurrency(s.balance)})); downloadAsPdf(filename, `Purchases Report`, headers, data); } }; const clearFilters = () => { setSearchTerm(''); setStartDate(''); setEndDate(''); setSelectedVendor(''); setSelectedStatus(''); }; return ( <>

Purchase Orders

search setSearchTerm(e.target.value)} />
setStartDate(e.target.value)} />
setEndDate(e.target.value)} />
{paginatedPurchases.map((purchase) => ( ))}
Order ID Vendor Date Total Amount Paid Balance Status
#{purchase.id} {purchase.vendorName} {purchase.date} {formatCurrency(purchase.totalAmount)} {formatCurrency(purchase.amountPaid)} {formatCurrency(purchase.balance)} {purchase.status}
setIsModalOpen(false)} onSave={handleSavePurchase} products={products} contacts={contacts} /> ); }; // --- ORDERS PAGE --- const AddOrderModal: React.FC<{ isOpen: boolean; onClose: () => void; onSave: (order: Order) => void; products: Product[]; contacts: Contact[]; }> = ({ isOpen, onClose, onSave, products, contacts }) => { const [customerId, setCustomerId] = useState(''); const [items, setItems] = useState([]); const [currentProductId, setCurrentProductId] = useState(''); const [currentQuantity, setCurrentQuantity] = useState(1); const [isSaving, setIsSaving] = useState(false); const customers = contacts.filter(c => c.role === 'Customer'); useEffect(() => { if (isOpen) { setIsSaving(false); setCustomerId(''); setItems([]); setCurrentProductId(''); setCurrentQuantity(1); } }, [isOpen]); const selectedProduct = products.find(p => p.id === currentProductId); const availableStock = selectedProduct ? selectedProduct.stock : 0; const handleAddItem = () => { if (!currentProductId || !currentQuantity || !selectedProduct) return; const existingItemIndex = items.findIndex(item => item.productId === currentProductId); const newQuantity = Number(currentQuantity); if (newQuantity > availableStock) { alert(`Cannot add ${newQuantity} items. Only ${availableStock} in stock.`); return; } let newItems = [...items]; if (existingItemIndex > -1) { newItems[existingItemIndex].quantity += newQuantity; newItems[existingItemIndex].total = newItems[existingItemIndex].quantity * newItems[existingItemIndex].price; } else { newItems.push({ productId: selectedProduct.id, name: selectedProduct.name, quantity: newQuantity, price: selectedProduct.price, total: newQuantity * selectedProduct.price, }); } setItems(newItems); setCurrentProductId(''); setCurrentQuantity(1); }; const handleRemoveItem = (productId: number) => { setItems(items.filter(item => item.productId !== productId)); }; const totalAmount = items.reduce((sum, item) => sum + item.total, 0); const handleSave = async () => { if (!customerId || items.length === 0) return; setIsSaving(true); try { const payload = { customerId, items: items.map(({ name, total, ...item}) => item) }; const savedOrder = await apiCall('orders.php', 'POST', { action: 'create', data: payload }); onSave(savedOrder); } catch (error) { console.error("Failed to save order", error); alert(`Error saving order: ${(error as Error).message}`); } finally { setIsSaving(false); } }; return ( } >

Add Products

setCurrentQuantity(Number(e.target.value))} disabled={!currentProductId}/>
{items.length > 0 && ( <> {items.map(item => ( ))}
ProductQtyPriceTotal
{item.name} {item.quantity} {formatCurrency(item.price)} {formatCurrency(item.total)}

Total: {formatCurrency(totalAmount)}

)}
); }; const ConvertToSaleModal: React.FC<{ isOpen: boolean; onClose: () => void; onConfirm: (sale: Sale, updatedProducts: Product[]) => void; order: Order | null; products: Product[]; }> = ({ isOpen, onClose, onConfirm, order, products }) => { const [amountPaid, setAmountPaid] = useState(''); const [errors, setErrors] = useState>(new Map()); const [isSaving, setIsSaving] = useState(false); useEffect(() => { if (isOpen && order) { setAmountPaid(''); setErrors(new Map()); setIsSaving(false); } }, [isOpen, order]); if (!order) return null; const handleConfirm = async () => { const newErrors = new Map(); let allValid = true; const itemsForSale: { productId: number; quantity: number }[] = []; for (const item of order.items) { const product = products.find(p => p.id === item.productId); const stock = product?.stock || 0; if (stock < item.quantity) { newErrors.set(item.productId, `Not enough stock. Available: ${stock}`); allValid = false; continue; } itemsForSale.push({ productId: item.productId, quantity: item.quantity }); } setErrors(newErrors); if (allValid) { setIsSaving(true); try { const payload = { orderId: order.id, amountPaid: Number(amountPaid) || 0, items: itemsForSale, }; const response = await apiCall<{ savedSale: Sale; updatedProducts: Product[] }>('orders.php', 'POST', { action: 'convertToSale', data: payload }); onConfirm(response.savedSale, response.updatedProducts); } catch(error) { console.error("Failed to convert order", error); alert(`Error converting order: ${(error as Error).message}`); } finally { setIsSaving(false); } } }; return ( } >

Confirm stock availability and enter payment details to convert this order into a formal sale.

{order.items.map(item => { const product = products.find(p => p.id === item.productId); const stock = product?.stock || 0; const error = errors.get(item.productId); return ( ) })}
ProductQty OrderedStock Available
{item.name} {item.quantity} {stock} {error &&
{error}
}

Customer: {order.customerName}

Total Amount: {formatCurrency(order.totalAmount)}

setAmountPaid(Number(e.target.value) >= 0 ? Number(e.target.value) : '')} min="0" max={order.totalAmount} />
); }; const OrdersPage: React.FC<{ orders: Order[]; setOrders: React.Dispatch>; setSales: React.Dispatch>; products: Product[]; setProducts: React.Dispatch>; contacts: Contact[]; }> = ({ orders, setOrders, setSales, products, setProducts, contacts }) => { const [isAddModalOpen, setIsAddModalOpen] = useState(false); const [isConvertModalOpen, setIsConvertModalOpen] = useState(false); const [orderToConvert, setOrderToConvert] = useState(null); const [searchTerm, setSearchTerm] = useState(''); const [currentPage, setCurrentPage] = useState(1); const itemsPerPage = 10; const filteredOrders = useMemo(() => { const searchKeywords = searchTerm.toLowerCase().split(' ').filter(k => k); if (searchKeywords.length === 0) return orders; return orders.filter(o => { const searchableText = `${o.customerName.toLowerCase()} #${o.id} ${o.status.toLowerCase()}`; return searchKeywords.every(keyword => searchableText.includes(keyword)); }); }, [orders, searchTerm]); const paginatedOrders = useMemo(() => { const startIndex = (currentPage - 1) * itemsPerPage; return filteredOrders.slice(startIndex, startIndex + itemsPerPage); }, [filteredOrders, currentPage, itemsPerPage]); useEffect(() => { setCurrentPage(1); }, [searchTerm]); const handleSaveOrder = (newOrder: Order) => { setOrders(prev => [newOrder, ...prev]); setIsAddModalOpen(false); }; const handleOpenConvertToSale = (order: Order) => { setOrderToConvert(order); setIsConvertModalOpen(true); }; const handleConfirmConversion = async (newSale: Sale, updatedProducts: Product[]) => { if (!orderToConvert) return; setSales(prev => [newSale, ...prev]); const updatedProductsMap = new Map(updatedProducts.map(p => [p.id, p])); setProducts(prev => prev.map(p => updatedProductsMap.get(p.id) || p)); setOrders(prev => prev.filter(o => o.id !== orderToConvert.id)); setIsConvertModalOpen(false); setOrderToConvert(null); }; return ( <>

Customer Orders

search setSearchTerm(e.target.value)} />
{paginatedOrders.map((order) => ( ))}
Order ID Customer Date Total Amount Actions
#{order.id} {order.customerName} {order.date} {formatCurrency(order.totalAmount)}
setIsAddModalOpen(false)} onSave={handleSaveOrder} products={products} contacts={contacts} /> setIsConvertModalOpen(false)} onConfirm={handleConfirmConversion} order={orderToConvert} products={products} /> ); }; // --- CONTACTS PAGE --- const ContactModal: React.FC<{ isOpen: boolean; onClose: () => void; onSave: (contact: Contact) => void; contactToEdit: Contact | null; }> = ({ isOpen, onClose, onSave, contactToEdit }) => { const [contact, setContact] = useState>({ name: '', email: '', role: 'Customer', creditLimit: 0 }); const [isSaving, setIsSaving] = useState(false); useEffect(() => { if (isOpen) { setIsSaving(false); setContact(contactToEdit || { name: '', email: '', role: 'Customer', creditLimit: 0 }); } }, [isOpen, contactToEdit]); const handleChange = (e: React.ChangeEvent) => { const { name, value } = e.target; if (name === 'creditLimit') { setContact(prev => ({ ...prev, [name]: parseFloat(value) || 0 })); } else { setContact(prev => ({ ...prev, [name]: value as any })); } }; const handleSave = async () => { setIsSaving(true); try { const payload = { ...contact, ...(contactToEdit && { id: contactToEdit.id }), }; const action = contactToEdit ? 'update' : 'create'; const savedContact = await apiCall('contacts.php', 'POST', { action, data: payload }); onSave(savedContact); } catch (error) { console.error("Failed to save contact", error); alert(`Error saving contact: ${(error as Error).message}`); } finally { setIsSaving(false); } }; return ( } >
{contact.role === 'Customer' && (
)}
); }; const ContactsPage: React.FC<{ contacts: Contact[]; setContacts: React.Dispatch>; sales: Sale[]; purchases: Purchase[]; }> = ({ contacts, setContacts, sales, purchases }) => { const [isModalOpen, setIsModalOpen] = useState(false); const [isConfirmOpen, setIsConfirmOpen] = useState(false); const [editingContact, setEditingContact] = useState(null); const [contactToDelete, setContactToDelete] = useState(null); const [searchTerm, setSearchTerm] = useState(''); const [currentPage, setCurrentPage] = useState(1); const [isDeleting, setIsDeleting] = useState(false); const itemsPerPage = 10; const filteredContacts = useMemo(() => { const searchKeywords = searchTerm.toLowerCase().split(' ').filter(k => k); if (searchKeywords.length === 0) return contacts; return contacts.filter(c => { const searchableText = `${c.name.toLowerCase()} ${c.email.toLowerCase()} ${c.role.toLowerCase()}`; return searchKeywords.every(keyword => searchableText.includes(keyword)); }); }, [contacts, searchTerm]); const paginatedContacts = useMemo(() => { const startIndex = (currentPage - 1) * itemsPerPage; return filteredContacts.slice(startIndex, startIndex + itemsPerPage); }, [filteredContacts, currentPage, itemsPerPage]); useEffect(() => { setCurrentPage(1); }, [searchTerm]); const calculateBalance = useCallback((contact: Contact) => { if (contact.role === 'Customer') { return sales .filter(s => s.customerId === contact.id) .reduce((total, sale) => total + sale.balance, 0); } else { return purchases .filter(p => p.vendorId === contact.id) .reduce((total, purchase) => total + purchase.balance, 0); } }, [sales, purchases]); const getContactStats = useCallback((contact: Contact) => { if (contact.role === 'Customer') { const customerSales = sales.filter(s => s.customerId === contact.id); return { totalOrders: customerSales.length, pendingOrders: customerSales.filter(s => s.status !== 'Paid').length, }; } if (contact.role === 'Vendor') { const vendorPurchases = purchases.filter(p => p.vendorId === contact.id); return { totalOrders: vendorPurchases.length, pendingOrders: vendorPurchases.filter(p => p.status !== 'Paid').length, }; } return { totalOrders: 0, pendingOrders: 0 }; }, [sales, purchases]); const handleOpenAddModal = () => { setEditingContact(null); setIsModalOpen(true); }; const handleOpenEditModal = (contact: Contact) => { setEditingContact(contact); setIsModalOpen(true); }; const handleOpenDeleteConfirm = (contact: Contact) => { setContactToDelete(contact); setIsConfirmOpen(true); }; const handleCloseModals = () => { setIsModalOpen(false); setIsConfirmOpen(false); setEditingContact(null); setContactToDelete(null); }; const handleSaveContact = (savedContact: Contact) => { if (editingContact) { setContacts(contacts.map(c => c.id === savedContact.id ? savedContact : c)); } else { setContacts([savedContact, ...contacts]); } handleCloseModals(); }; const handleConfirmDelete = async () => { if (contactToDelete) { setIsDeleting(true); try { await apiCall('contacts.php', 'POST', { action: 'delete', data: { id: contactToDelete.id } }); setContacts(contacts.filter(c => c.id !== contactToDelete.id)); } catch(error) { console.error("Failed to delete contact", error); alert(`Error deleting contact: ${(error as Error).message}`); } finally { setIsDeleting(false); handleCloseModals(); } } }; const handleExport = (format: 'csv' | 'pdf') => { const dataWithStats = contacts.map(c => { const { totalOrders, pendingOrders } = getContactStats(c); return { ...c, balance: calculateBalance(c), totalOrders, pendingOrders, } }); if(format === 'csv') { const headers = ['Name', 'Email', 'Role', 'Credit Limit', 'Total Orders', 'Pending Orders', 'Outstanding Balance']; const data = dataWithStats.map(c => [c.name, c.email, c.role, c.creditLimit || 0, c.totalOrders, c.pendingOrders, c.balance.toFixed(2)]); downloadAsCsv('contacts-report', headers, data); } else { const headers = [ { header: 'Name', dataKey: 'name' }, { header: 'Email', dataKey: 'email' }, { header: 'Role', dataKey: 'role' }, { header: 'Credit Limit', dataKey: 'creditLimit' }, { header: 'Total Orders', dataKey: 'totalOrders' }, { header: 'Pending Orders', dataKey: 'pendingOrders' }, { header: 'Outstanding Balance', dataKey: 'balance' }, ]; const data = dataWithStats.map(c => ({ ...c, creditLimit: formatCurrency(c.creditLimit || 0), balance: formatCurrency(c.balance) })); downloadAsPdf('contacts-report', 'Contacts Report', headers, data); } } return ( <>

Contacts

search setSearchTerm(e.target.value)} />
{searchTerm && (
Filtering by: "{searchTerm}"
)}
{paginatedContacts.map((contact) => { const { totalOrders, pendingOrders } = getContactStats(contact); return ( ); })}
Name Email Role Credit Limit Total Orders Pending Orders Outstanding Balance Actions
{contact.name} {contact.email} {contact.role} {contact.role === 'Customer' ? formatCurrency(contact.creditLimit || 0) : 'N/A'} {totalOrders} {pendingOrders} {formatCurrency(calculateBalance(contact))}
); }; // --- PAYMENTS PAGE --- const AddPaymentModal: React.FC<{ isOpen: boolean; onClose: () => void; onSave: (payment: Payment, updatedTransaction: Sale | Purchase) => void; sales: Sale[]; purchases: Purchase[]; }> = ({ isOpen, onClose, onSave, sales, purchases }) => { const [type, setType] = useState<'Sale' | 'Purchase'>('Sale'); const [transactionId, setTransactionId] = useState(''); const [amount, setAmount] = useState(''); const [method, setMethod] = useState('Cash'); const [reference, setReference] = useState(''); const [isSaving, setIsSaving] = useState(false); const transactionsWithBalance = useMemo(() => { if (type === 'Sale') { return sales.filter(s => s.balance > 0).map(s => ({ id: s.id, name: `#${s.id} - ${s.customerName}`, balance: s.balance })); } else { return purchases.filter(p => p.balance > 0).map(p => ({ id: p.id, name: `#${p.id} - ${p.vendorName}`, balance: p.balance })); } }, [type, sales, purchases]); useEffect(() => { if (isOpen) { setIsSaving(false); setType('Sale'); setTransactionId(''); setAmount(''); setMethod('Cash'); setReference(''); } }, [isOpen]); useEffect(() => { setTransactionId(''); setAmount(''); }, [type]); useEffect(() => { if (transactionId) { const selected = transactionsWithBalance.find(t => t.id === transactionId); if (selected) { setAmount(selected.balance); } } }, [transactionId, transactionsWithBalance]); const handleSave = async () => { if (!transactionId || !amount || Number(amount) <= 0) { alert('Please select a transaction and enter a valid amount.'); return; } setIsSaving(true); try { const payload = { type, transactionId, amount: Number(amount), method, reference }; const response = await apiCall<{ savedPayment: Payment; updatedTransaction: Sale | Purchase }>('payments.php', 'POST', { action: 'create', data: payload }); onSave(response.savedPayment, response.updatedTransaction); } catch (error) { console.error("Failed to save payment", error); alert(`Error saving payment: ${(error as Error).message}`); } finally { setIsSaving(false); } }; const maxAmount = transactionsWithBalance.find(t => t.id === transactionId)?.balance || 0; return ( } >
setAmount(Number(e.target.value))} min="0.01" max={maxAmount} disabled={!transactionId} />
setReference(e.target.value)} />
); }; const PaymentsPage: React.FC<{ payments: Payment[]; setPayments: React.Dispatch>; sales: Sale[]; setSales: React.Dispatch>; purchases: Purchase[]; setPurchases: React.Dispatch>; }> = ({ payments, setPayments, sales, setSales, purchases, setPurchases }) => { const [isModalOpen, setIsModalOpen] = useState(false); const [searchTerm, setSearchTerm] = useState(''); const [currentPage, setCurrentPage] = useState(1); const itemsPerPage = 10; const filteredPayments = useMemo(() => { const searchKeywords = searchTerm.toLowerCase().split(' ').filter(k => k); if (searchKeywords.length === 0) return payments; return payments.filter(p => { const searchableText = `${p.contactName.toLowerCase()} #${p.transactionId} ${p.method.toLowerCase()} ${p.reference.toLowerCase()}`; return searchKeywords.every(keyword => searchableText.includes(keyword)); }); }, [payments, searchTerm]); const paginatedPayments = useMemo(() => { const startIndex = (currentPage - 1) * itemsPerPage; return filteredPayments.slice(startIndex, startIndex + itemsPerPage); }, [filteredPayments, currentPage, itemsPerPage]); useEffect(() => { setCurrentPage(1); }, [searchTerm]); const handleSavePayment = (newPayment: Payment, updatedTransaction: Sale | Purchase) => { if (newPayment.type === 'Sale') { setSales(prev => prev.map(s => s.id === updatedTransaction.id ? updatedTransaction as Sale : s)); } else { setPurchases(prev => prev.map(p => p.id === updatedTransaction.id ? updatedTransaction as Purchase : p)); } setPayments([newPayment, ...payments]); setIsModalOpen(false); }; return ( <>

Payments

search setSearchTerm(e.target.value)} />
{searchTerm && (
Filtering by: "{searchTerm}"
)}
{paginatedPayments.map(p => ( ))}
Date Type Transaction ID Contact Amount Method Reference
{p.date} {p.type} #{p.transactionId} {p.contactName} {formatCurrency(p.amount)} {p.method} {p.reference || 'N/A'}
setIsModalOpen(false)} onSave={handleSavePayment} sales={sales} purchases={purchases} /> ); }; // --- USERS PAGE --- const UserModal: React.FC<{ isOpen: boolean; onClose: () => void; onSave: (user: User) => void; userToEdit: User | null; }> = ({ isOpen, onClose, onSave, userToEdit }) => { const [name, setName] = useState(''); const [email, setEmail] = useState(''); const [role, setRole] = useState('Staff'); const [password, setPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); const [isSaving, setIsSaving] = useState(false); useEffect(() => { if (isOpen) { setIsSaving(false); if (userToEdit) { setName(userToEdit.name); setEmail(userToEdit.email); setRole(userToEdit.role); } else { setName(''); setEmail(''); setRole('Staff'); } setPassword(''); setConfirmPassword(''); } }, [isOpen, userToEdit]); const handleSave = async () => { if (password) { if (password !== confirmPassword) { alert("Passwords do not match."); return; } } else if (!userToEdit) { alert("Password is required for new users."); return; } setIsSaving(true); try { const payload = { name, email, role, ...(password && { password }), ...(userToEdit && { id: userToEdit.id }), }; const action = userToEdit ? 'update' : 'create'; const savedUser = await apiCall('users.php', 'POST', { action, data: payload }); onSave(savedUser); } catch (error) { console.error("Failed to save user", error); alert(`Error saving user: ${(error as Error).message}`); } finally { setIsSaving(false); } }; return ( } >
setName(e.target.value)} />
setEmail(e.target.value)} />

{userToEdit ? 'Change Password' : 'Set Password'}

setPassword(e.target.value)} placeholder={userToEdit ? 'Leave blank to keep unchanged' : ''} />
setConfirmPassword(e.target.value)} />
); }; const UsersPage: React.FC<{ users: User[]; setUsers: React.Dispatch>; currentUser: User | null; }> = ({ users, setUsers, currentUser }) => { const [isModalOpen, setIsModalOpen] = useState(false); const [isConfirmOpen, setIsConfirmOpen] = useState(false); const [editingUser, setEditingUser] = useState(null); const [userToDelete, setUserToDelete] = useState(null); const [searchTerm, setSearchTerm] = useState(''); const [currentPage, setCurrentPage] = useState(1); const [isDeleting, setIsDeleting] = useState(false); const itemsPerPage = 10; const filteredUsers = useMemo(() => { const searchKeywords = searchTerm.toLowerCase().split(' ').filter(k => k); if (searchKeywords.length === 0) return users; return users.filter(u => { const searchableText = `${u.name.toLowerCase()} ${u.email.toLowerCase()}`; return searchKeywords.every(keyword => searchableText.includes(keyword)); }); }, [users, searchTerm]); const paginatedUsers = useMemo(() => { const startIndex = (currentPage - 1) * itemsPerPage; return filteredUsers.slice(startIndex, startIndex + itemsPerPage); }, [filteredUsers, currentPage, itemsPerPage]); useEffect(() => { setCurrentPage(1); }, [searchTerm]); const handleOpenAddModal = () => { setEditingUser(null); setIsModalOpen(true); }; const handleOpenEditModal = (user: User) => { setEditingUser(user); setIsModalOpen(true); }; const handleOpenDeleteConfirm = (user: User) => { setUserToDelete(user); setIsConfirmOpen(true); }; const handleCloseModals = () => { setIsModalOpen(false); setIsConfirmOpen(false); setEditingUser(null); setUserToDelete(null); }; const handleSaveUser = (savedUser: User) => { if (editingUser) { setUsers(users.map(u => u.id === savedUser.id ? savedUser : u)); } else { setUsers([savedUser, ...users]); } handleCloseModals(); }; const handleConfirmDelete = async () => { if (userToDelete) { if (userToDelete.id === currentUser?.id) { alert("You cannot delete the currently active user."); handleCloseModals(); return; } setIsDeleting(true); try { await apiCall('users.php', 'POST', { action: 'delete', data: { id: userToDelete.id } }); setUsers(users.filter(u => u.id !== userToDelete.id)); } catch (error) { console.error("Failed to delete user", error); alert(`Error deleting user: ${(error as Error).message}`); } finally { setIsDeleting(false); handleCloseModals(); } } }; return ( <>

User Management

search setSearchTerm(e.target.value)} />
{searchTerm && (
Filtering by: "{searchTerm}"
)}
{paginatedUsers.map((user) => ( ))}
Name Email Role Actions
{user.name}{user.id === currentUser?.id && ' (You)'} {user.email} {user.role}
); }; // --- AUDIT TRAIL PAGE --- const AuditTrailPage: React.FC<{ log: AuditLogEntry[] }> = ({ log }) => { const [searchTerm, setSearchTerm] = useState(''); const [currentPage, setCurrentPage] = useState(1); const itemsPerPage = 15; const filteredLog = useMemo(() => { const searchKeywords = searchTerm.toLowerCase().split(' ').filter(k => k); if (searchKeywords.length === 0) return log; return log.filter(entry => { const searchableText = `${entry.userName.toLowerCase()} ${entry.action.toLowerCase()} ${entry.details.toLowerCase()}`; return searchKeywords.every(keyword => searchableText.includes(keyword)); }); }, [log, searchTerm]); const paginatedLog = useMemo(() => { const startIndex = (currentPage - 1) * itemsPerPage; return filteredLog.slice(startIndex, startIndex + itemsPerPage); }, [filteredLog, currentPage, itemsPerPage]); useEffect(() => { setCurrentPage(1); }, [searchTerm]); return ( <>

Audit Trail

search setSearchTerm(e.target.value)} />
{searchTerm && (
Filtering by: "{searchTerm}"
)}
{paginatedLog.map((entry) => ( ))}
Timestamp User Action Details
{new Date(entry.timestamp).toLocaleString()} {entry.userName} {entry.action} {entry.details}
); }; // --- EXPENSES PAGE --- const ManageCategoriesModal: React.FC<{ isOpen: boolean; onClose: () => void; categories: ExpenseCategory[]; setCategories: React.Dispatch>; expenses: Expense[]; }> = ({ isOpen, onClose, categories, setCategories, expenses }) => { const [newCategoryName, setNewCategoryName] = useState(''); const [editingCategoryId, setEditingCategoryId] = useState(null); const [editingCategoryName, setEditingCategoryName] = useState(''); const [selectedCategoryIds, setSelectedCategoryIds] = useState>(new Set()); const [isConfirmOpen, setIsConfirmOpen] = useState(false); useEffect(() => { if (!isOpen) { setSelectedCategoryIds(new Set()); setIsConfirmOpen(false); } }, [isOpen]); const handleAddCategory = async () => { if (!newCategoryName.trim()) return; try { const newCategory = await apiCall('expenses.php', 'POST', { action: 'createCategory', data: { name: newCategoryName.trim() }}); setCategories(prev => [...prev, newCategory]); setNewCategoryName(''); } catch (error) { alert(`Error: ${(error as Error).message}`); } }; const handleDeleteCategory = async (categoryId: number) => { const isUsed = expenses.some(e => e.categoryId === categoryId); if (isUsed) { alert('Cannot delete a category that is currently in use.'); return; } try { await apiCall('expenses.php', 'POST', { action: 'deleteCategory', data: { id: categoryId }}); setCategories(prev => prev.filter(c => c.id !== categoryId)); } catch (error) { alert(`Error: ${(error as Error).message}`); } }; const handleStartEdit = (category: ExpenseCategory) => { setEditingCategoryId(category.id); setEditingCategoryName(category.name); }; const handleCancelEdit = () => { setEditingCategoryId(null); setEditingCategoryName(''); }; const handleSaveEdit = async () => { if (!editingCategoryId || !editingCategoryName.trim()) return; try { const updatedCategory = await apiCall('expenses.php', 'POST', { action: 'updateCategory', data: { id: editingCategoryId, name: editingCategoryName.trim() }}); setCategories(prev => prev.map(c => c.id === editingCategoryId ? updatedCategory : c)); } catch (error) { alert(`Error: ${(error as Error).message}`); } handleCancelEdit(); }; const handleToggleSelect = (categoryId: number) => { setSelectedCategoryIds(prev => { const newSelection = new Set(prev); if (newSelection.has(categoryId)) { newSelection.delete(categoryId); } else { newSelection.add(categoryId); } return newSelection; }); }; const handleOpenBulkDeleteConfirm = () => { if (selectedCategoryIds.size > 0) { setIsConfirmOpen(true); } }; const handleConfirmBulkDelete = async () => { try { await apiCall('expenses.php', 'POST', { action: 'bulkDeleteCategories', data: { ids: Array.from(selectedCategoryIds) }}); setCategories(prev => prev.filter(c => !selectedCategoryIds.has(c.id))); } catch (error) { alert(`Error: ${(error as Error).message}`); } finally { setSelectedCategoryIds(new Set()); setIsConfirmOpen(false); } }; return ( <> 0 && (
{selectedCategoryIds.size} selected
)} >
setNewCategoryName(e.target.value)} placeholder="e.g., Utilities" />
    {categories.map(category => (
  • {editingCategoryId === category.id ? ( <> setEditingCategoryName(e.target.value)} className="category-edit-input" autoFocus />
    ) : ( <>
    handleToggleSelect(category.id)} aria-label={`Select category ${category.name}`} /> {category.name}
    )}
  • ))}
setIsConfirmOpen(false)} onConfirm={handleConfirmBulkDelete} title="Confirm Bulk Deletion" message={`Are you sure you want to delete the ${selectedCategoryIds.size} selected categories? Categories currently in use will not be deleted.`} /> ); }; const ExpenseModal: React.FC<{ isOpen: boolean; onClose: () => void; onSave: (expense: Expense) => void; expenseToEdit: Expense | null; categories: ExpenseCategory[]; }> = ({ isOpen, onClose, onSave, expenseToEdit, categories }) => { const [expense, setExpense] = useState({ date: '', description: '', categoryId: '', amount: '' }); const [isSaving, setIsSaving] = useState(false); useEffect(() => { if (isOpen) { setIsSaving(false); if (expenseToEdit) { setExpense({ date: expenseToEdit.date, description: expenseToEdit.description, categoryId: String(expenseToEdit.categoryId), amount: String(expenseToEdit.amount), }); } else { setExpense({ date: new Date().toISOString().split('T')[0], description: '', categoryId: '', amount: '' }); } } }, [isOpen, expenseToEdit]); const handleChange = (e: React.ChangeEvent) => { const { name, value } = e.target; setExpense(prev => ({ ...prev, [name]: value })); }; const handleSave = async () => { if (!expense.date || !expense.description || !expense.categoryId || !expense.amount) { alert('Please fill out all fields.'); return; } setIsSaving(true); try { const payload = { date: expense.date, description: expense.description, categoryId: Number(expense.categoryId), amount: parseFloat(expense.amount), ...(expenseToEdit && { id: expenseToEdit.id }), }; const action = expenseToEdit ? 'update' : 'create'; const savedExpense = await apiCall('expenses.php', 'POST', { action, data: payload }); onSave(savedExpense); } catch (error) { alert(`Error: ${(error as Error).message}`); } finally { setIsSaving(false); } }; return ( } >
); }; const ExpensesPage: React.FC<{ expenses: Expense[]; setExpenses: React.Dispatch>; categories: ExpenseCategory[]; setCategories: React.Dispatch>; }> = ({ expenses, setExpenses, categories, setCategories }) => { const [isModalOpen, setIsModalOpen] = useState(false); const [isCategoryModalOpen, setIsCategoryModalOpen] = useState(false); const [isConfirmOpen, setIsConfirmOpen] = useState(false); const [editingExpense, setEditingExpense] = useState(null); const [expenseToDelete, setExpenseToDelete] = useState(null); const [searchTerm, setSearchTerm] = useState(''); const [currentPage, setCurrentPage] = useState(1); const [isDeleting, setIsDeleting] = useState(false); const itemsPerPage = 10; const filteredExpenses = useMemo(() => { const searchKeywords = searchTerm.toLowerCase().split(' ').filter(k => k); if (searchKeywords.length === 0) return expenses; return expenses.filter(e => { const searchableText = `${e.description.toLowerCase()} ${e.categoryName.toLowerCase()} ${e.amount}`; return searchKeywords.every(keyword => searchableText.includes(keyword)); }); }, [expenses, searchTerm]); const paginatedExpenses = useMemo(() => { const startIndex = (currentPage - 1) * itemsPerPage; return filteredExpenses.slice(startIndex, startIndex + itemsPerPage); }, [filteredExpenses, currentPage, itemsPerPage]); useEffect(() => { setCurrentPage(1); }, [searchTerm]); const handleOpenAddModal = () => { setEditingExpense(null); setIsModalOpen(true); }; const handleOpenEditModal = (expense: Expense) => { setEditingExpense(expense); setIsModalOpen(true); }; const handleOpenDeleteConfirm = (expense: Expense) => { setExpenseToDelete(expense); setIsConfirmOpen(true); }; const handleCloseModals = () => { setIsModalOpen(false); setIsConfirmOpen(false); }; const handleSaveExpense = (savedExpense: Expense) => { if (editingExpense) { // Editing setExpenses(expenses.map(e => e.id === savedExpense.id ? savedExpense : e)); } else { // Adding setExpenses([savedExpense, ...expenses]); } handleCloseModals(); }; const handleConfirmDelete = async () => { if (expenseToDelete) { setIsDeleting(true); try { await apiCall('expenses.php', 'POST', { action: 'delete', data: { id: expenseToDelete.id }}); setExpenses(expenses.filter(e => e.id !== expenseToDelete.id)); } catch (error) { alert(`Error: ${(error as Error).message}`); } finally { setIsDeleting(false); setIsConfirmOpen(false); setExpenseToDelete(null); } } }; return ( <>

Expenses

search setSearchTerm(e.target.value)} />
{searchTerm && (
Filtering by: "{searchTerm}"
)}
{paginatedExpenses.map((expense) => ( ))}
Date Description Category Amount Actions
{expense.date} {expense.description} {expense.categoryName} {formatCurrency(expense.amount)}
setIsCategoryModalOpen(false)} categories={categories} setCategories={setCategories} expenses={expenses} /> setIsConfirmOpen(false)} onConfirm={handleConfirmDelete} isSaving={isDeleting} title="Confirm Deletion" message={`Are you sure you want to delete this expense: "${expenseToDelete?.description}"? This action cannot be undone.`} /> ); }; // --- RETURNS PAGE --- const AddReturnModal: React.FC<{ isOpen: boolean; onClose: () => void; onSave: (newReturn: Return, updatedSale: Sale, updatedProducts: Product[]) => void; sales: Sale[]; returns: Return[]; }> = ({ isOpen, onClose, onSave, sales, returns }) => { const [selectedSaleId, setSelectedSaleId] = useState(''); const [itemsToReturn, setItemsToReturn] = useState>(new Map()); const [isSaving, setIsSaving] = useState(false); const sale = useMemo(() => sales.find(s => s.id === selectedSaleId), [selectedSaleId, sales]); const previouslyReturnedQuantities = useMemo(() => { if (!sale) return new Map(); const returnedMap = new Map(); returns .filter(r => r.saleId === sale.id) .forEach(r => { r.items.forEach(item => { returnedMap.set(item.productId, (returnedMap.get(item.productId) || 0) + item.quantity); }); }); return returnedMap; }, [sale, returns]); useEffect(() => { if (isOpen) { setIsSaving(false); setSelectedSaleId(''); setItemsToReturn(new Map()); } }, [isOpen]); useEffect(() => { setItemsToReturn(new Map()); }, [selectedSaleId]); const handleQuantityChange = (productId: number, quantity: number, maxQuantity: number) => { const newQuantity = Math.max(0, Math.min(quantity, maxQuantity)); setItemsToReturn(prev => new Map(prev).set(productId, newQuantity)); }; const handleSave = async () => { if (!sale) return; const returnItemsPayload = Array.from(itemsToReturn.entries()) .filter(([, quantity]) => quantity > 0) .map(([productId, quantity]) => ({ productId, quantity })); if (returnItemsPayload.length === 0) { alert("No items selected for return."); return; } setIsSaving(true); try { const payload = { saleId: sale.id, items: returnItemsPayload, }; const response = await apiCall<{ newReturn: Return; updatedSale: Sale; updatedProducts: Product[]; }>('returns.php', 'POST', { action: 'create', data: payload }); onSave(response.newReturn, response.updatedSale, response.updatedProducts); } catch (error) { console.error("Failed to save return", error); alert(`Error saving return: ${(error as Error).message}`); } finally { setIsSaving(false); } }; return ( } >
{sale && (

Items from Sale #{sale.id}

{sale.items.map(item => { const returnedQty = previouslyReturnedQuantities.get(item.productId) || 0; const availableToReturn = item.quantity - returnedQty; return ( ) })}
Product Qty Sold Price Qty to Return
{item.name} {item.quantity} {formatCurrency(item.price)} handleQuantityChange(item.productId, parseInt(e.target.value) || 0, availableToReturn)} min="0" max={availableToReturn} disabled={availableToReturn <= 0} placeholder="0" /> (Max: {availableToReturn})
)}
); }; const ReturnsPage: React.FC<{ returns: Return[]; setReturns: React.Dispatch>; sales: Sale[]; setSales: React.Dispatch>; products: Product[]; setProducts: React.Dispatch>; }> = ({ returns, setReturns, sales, setSales, products, setProducts }) => { const [isModalOpen, setIsModalOpen] = useState(false); const [searchTerm, setSearchTerm] = useState(''); const [currentPage, setCurrentPage] = useState(1); const itemsPerPage = 10; const filteredReturns = useMemo(() => { if (!searchTerm) return returns; const searchLower = searchTerm.toLowerCase(); return returns.filter(r => r.customerName.toLowerCase().includes(searchLower) || String(r.saleId).includes(searchLower) ); }, [returns, searchTerm]); const paginatedReturns = useMemo(() => { const startIndex = (currentPage - 1) * itemsPerPage; return filteredReturns.slice(startIndex, startIndex + itemsPerPage); }, [filteredReturns, currentPage]); const handleSaveReturn = (newReturn: Return, updatedSale: Sale, updatedProducts: Product[]) => { setReturns(prev => [newReturn, ...prev]); const updatedProductsMap = new Map(updatedProducts.map(p => [p.id, p])); setProducts(prev => prev.map(p => updatedProductsMap.get(p.id) || p)); setSales(prev => prev.map(s => s.id === updatedSale.id ? updatedSale : s)); setIsModalOpen(false); }; return ( <>

Sales Returns

search setSearchTerm(e.target.value)} />
{paginatedReturns.map(r => ( ))}
Return ID Date Original Sale ID Customer Total Value
#{r.id} {r.date} #{r.saleId} {r.customerName} {formatCurrency(r.totalValue)}
setIsModalOpen(false)} onSave={handleSaveReturn} sales={sales} returns={returns} /> ); }; // --- DAMAGES PAGE --- const AddDamageModal: React.FC<{ isOpen: boolean; onClose: () => void; onSave: (damage: Damage, updatedProduct: Product) => void; products: Product[]; }> = ({ isOpen, onClose, onSave, products }) => { const [productId, setProductId] = useState(''); const [quantity, setQuantity] = useState(''); const [reason, setReason] = useState(''); const [isSaving, setIsSaving] = useState(false); const product = useMemo(() => products.find(p => p.id === productId), [productId, products]); useEffect(() => { if (isOpen) { setIsSaving(false); setProductId(''); setQuantity(''); setReason(''); } }, [isOpen]); const handleSave = async () => { if (!productId || !quantity || !reason.trim() || !product) { alert('Please fill all fields.'); return; } setIsSaving(true); try { const payload = { productId, quantity: Number(quantity), reason }; const response = await apiCall<{ savedDamage: Damage; updatedProduct: Product }>('damages.php', 'POST', { action: 'create', data: payload }); onSave(response.savedDamage, response.updatedProduct); } catch(error) { console.error("Failed to log damage", error); alert(`Error logging damage: ${(error as Error).message}`); } finally { setIsSaving(false); } }; const stockAvailable = product?.stock || 0; return ( } >
setQuantity(Number(e.target.value))} min="1" max={stockAvailable} disabled={!productId} />
); }; const DamagesPage: React.FC<{ damages: Damage[]; setDamages: React.Dispatch>; products: Product[]; setProducts: React.Dispatch>; }> = ({ damages, setDamages, products, setProducts }) => { const [isModalOpen, setIsModalOpen] = useState(false); const [searchTerm, setSearchTerm] = useState(''); const [currentPage, setCurrentPage] = useState(1); const itemsPerPage = 10; const filteredDamages = useMemo(() => { if (!searchTerm) return damages; const searchLower = searchTerm.toLowerCase(); return damages.filter(d => d.productName.toLowerCase().includes(searchLower) || d.reason.toLowerCase().includes(searchLower) ); }, [damages, searchTerm]); const paginatedDamages = useMemo(() => { const startIndex = (currentPage - 1) * itemsPerPage; return filteredDamages.slice(startIndex, startIndex + itemsPerPage); }, [filteredDamages, currentPage]); const handleSaveDamage = (damage: Damage, updatedProduct: Product) => { setDamages(prev => [damage, ...prev]); setProducts(prev => prev.map(p => p.id === updatedProduct.id ? updatedProduct : p)); setIsModalOpen(false); }; return ( <>

Damaged Goods

search setSearchTerm(e.target.value)} />
{paginatedDamages.map(d => ( ))}
Log ID Date Product Quantity Reason
#{d.id} {d.date} {d.productName} {d.quantity} {d.reason}
setIsModalOpen(false)} onSave={handleSaveDamage} products={products} /> ); }; // --- REPORTS PAGE / DASHBOARD --- type DateRangePreset = 'week' | 'month' | 'quarter' | 'year' | 'custom'; const formatDate = (date: Date): string => date.toISOString().split('T')[0]; const getDateRange = (preset: DateRangePreset): { start: string, end: string } => { const now = new Date(); let start = new Date(now); const end = new Date(now); switch (preset) { case 'week': const dayOfWeek = now.getDay(); // 0 = Sunday start.setDate(now.getDate() - dayOfWeek); break; case 'month': start = new Date(now.getFullYear(), now.getMonth(), 1); break; case 'quarter': const quarter = Math.floor(now.getMonth() / 3); start = new Date(now.getFullYear(), quarter * 3, 1); break; case 'year': start = new Date(now.getFullYear(), 0, 1); break; case 'custom': // Should be handled by custom inputs return { start: '', end: '' }; } return { start: formatDate(start), end: formatDate(end) }; }; const DateRangeFilter: React.FC<{ onDateChange: (startDate: string, endDate: string) => void; }> = ({ onDateChange }) => { const [activePreset, setActivePreset] = useState('month'); const [customStart, setCustomStart] = useState(''); const [customEnd, setCustomEnd] = useState(''); const stableOnDateChange = useCallback(onDateChange, []); useEffect(() => { const initialRange = getDateRange('month'); stableOnDateChange(initialRange.start, initialRange.end); setCustomStart(initialRange.start); setCustomEnd(initialRange.end); }, [stableOnDateChange]); const handlePresetClick = (preset: DateRangePreset) => { setActivePreset(preset); if (preset !== 'custom') { const { start, end } = getDateRange(preset); onDateChange(start, end); setCustomStart(start); setCustomEnd(end); } }; useEffect(() => { if (activePreset === 'custom' && customStart && customEnd) { onDateChange(customStart, customEnd); } }, [customStart, customEnd, activePreset, onDateChange]); const presets: { id: DateRangePreset, label: string }[] = [ { id: 'week', label: 'This Week' }, { id: 'month', label: 'This Month' }, { id: 'quarter', label: 'This Quarter' }, { id: 'year', label: 'This Year' }, ]; return (
{presets.map(p => ( ))}
{ setCustomStart(e.target.value); setActivePreset('custom'); }} /> to { setCustomEnd(e.target.value); setActivePreset('custom'); }} />
); }; const SalesByMonthChart: React.FC<{ data: { month: string; totalSales: number }[] }> = ({ data }) => { const maxValue = useMemo(() => { if (data.length === 0) return 0; return Math.max(...data.map(d => d.totalSales)); }, [data]); if (data.length === 0) { return

No sales data for this period.

; } return (
{data.map(item => (
{`Rs.${(item.totalSales / 1000).toFixed(1)}k`}
0 ? (item.totalSales / maxValue) * 100 : 0}%` }} />
{item.month}
))}
); }; const TopProductsChart: React.FC<{ data: { name: string; quantity: number }[] }> = ({ data }) => { const maxValue = useMemo(() => { if (data.length === 0) return 0; return Math.max(...data.map(d => d.quantity)); }, [data]); if (data.length === 0) { return

No products sold in this period.

; } return (
{data.map(product => (
{product.name}
0 ? (product.quantity / maxValue) * 100 : 0}%` }} />
{product.quantity.toLocaleString()}
))}
); }; const ReportsDashboardPage: React.FC<{ products: Product[]; sales: Sale[]; purchases: Purchase[]; contacts: Contact[]; expenses: Expense[]; }> = ({ products, sales, purchases, contacts, expenses }) => { const [startDate, setStartDate] = useState(''); const [endDate, setEndDate] = useState(''); const [selectedCustomerId, setSelectedCustomerId] = useState(''); const customers = useMemo(() => contacts.filter(c => c.role === 'Customer'), [contacts]); const handleDateChange = useCallback((start: string, end: string) => { setStartDate(start); setEndDate(end); }, []); const { filteredSales, filteredPurchases, filteredExpenses } = useMemo(() => { if (!startDate || !endDate) return { filteredSales: [], filteredPurchases: [], filteredExpenses: [] }; const start = startDate; const end = endDate; return { filteredSales: sales.filter(s => s.date >= start && s.date <= end), filteredPurchases: purchases.filter(p => p.date >= start && p.date <= end), filteredExpenses: expenses.filter(e => e.date >= start && e.date <= end), }; }, [startDate, endDate, sales, purchases, expenses]); const { totalRevenue, cogs, totalExpenses, netProfit, } = useMemo(() => { const revenue = filteredSales.reduce((sum, s) => sum + s.totalAmount, 0); const cost = filteredPurchases.reduce((sum, p) => sum + p.totalAmount, 0); const expenseTotal = filteredExpenses.reduce((sum, e) => sum + e.amount, 0); const profit = revenue - cost - expenseTotal; return { totalRevenue: revenue, cogs: cost, totalExpenses: expenseTotal, netProfit: profit }; }, [filteredSales, filteredPurchases, filteredExpenses]); const accountsReceivable = useMemo(() => { return contacts .filter(c => c.role === 'Customer') .map(c => { const balance = sales .filter(s => s.customerId === c.id) .reduce((sum, s) => sum + s.balance, 0); return { ...c, balance }; }) .filter(c => c.balance > 0) .sort((a,b) => b.balance - a.balance); }, [contacts, sales]); const salesByCustomer = useMemo(() => { if (!selectedCustomerId) return []; return filteredSales.filter(s => s.customerId === selectedCustomerId); }, [filteredSales, selectedCustomerId]); const stockAgingData = useMemo(() => { const lastActivityMap = new Map(); [...sales, ...purchases].forEach(transaction => { const date = transaction.date; transaction.items.forEach(item => { const existingDate = lastActivityMap.get(item.productId); if (!existingDate || date > existingDate) { lastActivityMap.set(item.productId, date); } }); }); const detailedAging = products.map(product => { const lastActivityDate = lastActivityMap.get(product.id); const today = new Date(); const daysSinceActivity = lastActivityDate ? Math.floor((today.getTime() - new Date(lastActivityDate).getTime()) / (1000 * 3600 * 24)) : Infinity; const totalStock = product.stock; let bracket = '90+ days'; if (daysSinceActivity <= 30) bracket = '0-30 days'; else if (daysSinceActivity <= 60) bracket = '31-60 days'; else if (daysSinceActivity <= 90) bracket = '61-90 days'; return { ...product, value: totalStock * product.price, lastActivity: lastActivityDate || 'N/A', ageBracket: totalStock > 0 ? bracket : 'N/A', }; }); const summary = detailedAging.reduce((acc, product) => { if (product.stock > 0) { if (!acc[product.ageBracket]) { acc[product.ageBracket] = { quantity: 0, value: 0 }; } acc[product.ageBracket].quantity += product.stock; acc[product.ageBracket].value += product.value; } return acc; }, {} as Record); const summaryArray = ['0-30 days', '31-60 days', '61-90 days', '90+ days'].map(bracket => ({ bracket, ...summary[bracket] || { quantity: 0, value: 0 }, })); return { detailed: detailedAging.filter(p=>p.stock > 0), summary: summaryArray }; }, [products, sales, purchases]); const salesByMonthData = useMemo(() => { const monthlySales = filteredSales.reduce((acc, sale) => { const month = sale.date.substring(0, 7); // YYYY-MM acc[month] = (acc[month] || 0) + sale.totalAmount; return acc; }, {} as Record); return Object.entries(monthlySales) .map(([month, totalSales]) => ({ month: new Date(month + '-02').toLocaleString('default', { month: 'short', year: 'numeric' }), totalSales })) .sort((a,b) => new Date(a.month).getTime() - new Date(b.month).getTime()); }, [filteredSales]); const topSellingProductsData = useMemo(() => { const productQuantities = filteredSales .flatMap(sale => sale.items) .reduce((acc, item) => { acc[item.productId] = (acc[item.productId] || 0) + item.quantity; return acc; }, {} as Record); return Object.entries(productQuantities) .sort(([, qtyA], [, qtyB]) => qtyB - qtyA) .slice(0, 5) .map(([productId, quantity]) => ({ name: products.find(p => p.id === Number(productId))?.name || 'Unknown Product', quantity, })); }, [filteredSales, products]); const handleExport = (report: 'salesByCustomer' | 'stockAging' | 'cashFlow' | 'salesByMonth' | 'topSellingProducts', format: 'csv' | 'pdf') => { const filename = `${report}-report-${startDate}-to-${endDate}`; if (report === 'salesByCustomer') { if (!selectedCustomerId) return; const customer = customers.find(c => c.id === selectedCustomerId); if (!customer) return; const title = `Sales for ${customer.name} (${startDate} to ${endDate})`; if (format === 'csv') { const headers = ['Invoice ID', 'Date', 'Total Amount', 'Status']; const data = salesByCustomer.map(s => [s.id, s.date, s.totalAmount.toFixed(2), s.status]); downloadAsCsv(filename, headers, data); } else { const headers = [ { header: 'Invoice ID', dataKey: 'id' }, { header: 'Date', dataKey: 'date' }, { header: 'Total Amount', dataKey: 'totalAmount' }, { header: 'Status', dataKey: 'status' } ]; const data = salesByCustomer.map(s => ({...s, totalAmount: formatCurrency(s.totalAmount)})); downloadAsPdf(filename, title, headers, data); } } else if (report === 'stockAging') { const title = 'Stock Aging Report'; if (format === 'csv') { const headers = ['SKU', 'Product Name', 'Stock', 'Value', 'Last Activity', 'Age Bracket']; const data = stockAgingData.detailed.map(p => [p.sku, p.name, p.stock, p.value.toFixed(2), p.lastActivity, p.ageBracket]); downloadAsCsv('stock-aging-report', headers, data); } else { const headers = [ { header: 'SKU', dataKey: 'sku' }, { header: 'Product Name', dataKey: 'name' }, { header: 'Stock', dataKey: 'stock' }, { header: 'Value', dataKey: 'value' }, { header: 'Last Activity', dataKey: 'lastActivity' }, { header: 'Age Bracket', dataKey: 'ageBracket' } ]; const data = stockAgingData.detailed.map(p => ({ ...p, value: formatCurrency(p.value)})); downloadAsPdf('stock-aging-report', title, headers, data); } } else if (report === 'cashFlow') { const title = `Cash Flow Overview (${startDate} to ${endDate})`; if(format === 'csv') { const headers = ['Metric', 'Amount']; const data = [['Total Revenue', totalRevenue.toFixed(2)], ['Cost of Goods Sold', cogs.toFixed(2)], ['Net Flow', (totalRevenue - cogs).toFixed(2)]]; downloadAsCsv(filename, headers, data); } else { const headers = [{ header: 'Metric', dataKey: 'metric' }, { header: 'Amount', dataKey: 'amount' }]; const data = [ { metric: 'Total Revenue', amount: formatCurrency(totalRevenue)}, { metric: 'Cost of Goods Sold', amount: formatCurrency(cogs)}, { metric: 'Net Flow', amount: formatCurrency(totalRevenue - cogs)}, ]; downloadAsPdf(filename, title, headers, data); } } else if (report === 'salesByMonth') { const title = `Sales by Month (${startDate} to ${endDate})`; if (format === 'csv') { const headers = ['Month', 'Total Sales']; const data = salesByMonthData.map(item => [item.month, item.totalSales.toFixed(2)]); downloadAsCsv(filename, headers, data); } else { const headers = [{ header: 'Month', dataKey: 'month' }, { header: 'Total Sales', dataKey: 'totalSales' }]; const data = salesByMonthData.map(item => ({ ...item, totalSales: formatCurrency(item.totalSales) })); downloadAsPdf(filename, title, headers, data); } } else if (report === 'topSellingProducts') { const title = `Top Selling Products (${startDate} to ${endDate})`; if (format === 'csv') { const headers = ['Product Name', 'Quantity Sold']; const data = topSellingProductsData.map(item => [item.name, item.quantity]); downloadAsCsv(filename, headers, data); } else { const headers = [{ header: 'Product Name', dataKey: 'name' }, { header: 'Quantity Sold', dataKey: 'quantity' }]; downloadAsPdf(filename, title, headers, topSellingProductsData); } } }; const getStatusClass = (status: Sale['status']) => { if (status === 'Paid') return 'status-paid'; if (status === 'Partially Paid') return 'status-partial'; if (status === 'Unpaid') return 'status-unpaid'; return ''; }; return ( <>

Reports Dashboard

Total Revenue

{formatCurrency(totalRevenue)}

Cost of Goods Sold

{formatCurrency(cogs)}

Total Expenses

{formatCurrency(totalExpenses)}

= 0 ? 'positive' : 'negative'}`}>

Net Profit

{formatCurrency(netProfit)}

Sales by Month

Top Selling Products

Sales by Customer

{selectedCustomerId ? ( {salesByCustomer.length > 0 ? salesByCustomer.map(sale => ( )) : ( )}
Invoice ID Date Total Amount Status
#{sale.id} {sale.date} {formatCurrency(sale.totalAmount)} {sale.status}
No sales for this customer in the selected period.
) : (

Select a customer to view their sales report.

)}

Accounts Receivable

{accountsReceivable.length > 0 ? accountsReceivable.map(c => ( )) : ( )}
CustomerBalance
{c.name} {formatCurrency(c.balance)}
No outstanding balances.

Stock Aging

{stockAgingData.summary.map(item => ( ))}
Age BracketQuantityValue
{item.bracket} {item.quantity.toLocaleString()} {formatCurrency(item.value)}

Sales vs. Purchases

Total Revenue {formatCurrency(totalRevenue)}
Cost of Goods Sold {formatCurrency(cogs)}
Net Flow = 0 ? 'positive' : 'negative'}>{formatCurrency(totalRevenue - cogs)}
); }; // --- LAYOUT COMPONENTS --- const Sidebar: React.FC<{ currentPage: Page; setCurrentPage: (page: Page) => void; collapsed: boolean; currentUser: User | null; }> = ({ currentPage, setCurrentPage, collapsed, currentUser }) => { const visibleNavItems = useMemo(() => { if (!currentUser) return []; return navItems.filter(item => permissions.canViewPage(currentUser.role, item.id)); }, [currentUser]); return ( ); }; const Header: React.FC<{ user: User | null; theme: Theme; toggleTheme: () => void; toggleSidebar: () => void; onProfileClick: () => void; onLogout: () => void; }> = ({ user, theme, toggleTheme, toggleSidebar, onProfileClick, onLogout }) => { const [isDropdownOpen, setIsDropdownOpen] = useState(false); const dropdownRef = useRef(null); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { setIsDropdownOpen(false); } }; document.addEventListener("mousedown", handleClickOutside); return () => { document.removeEventListener("mousedown", handleClickOutside); }; }, [dropdownRef]); return (
{isDropdownOpen && (
)}
); }; // --- AUTHENTICATION COMPONENTS --- const ForgotPasswordModal: React.FC<{ isOpen: boolean; onClose: () => void; onSuccess: (token: string) => void; }> = ({ isOpen, onClose, onSuccess }) => { const [email, setEmail] = useState(''); const [isSent, setIsSent] = useState(false); const [token, setToken] = useState(''); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(''); useEffect(() => { if(isOpen) { setEmail(''); setIsSent(false); setToken(''); setIsLoading(false); setError(''); } }, [isOpen]); const handleSendLink = async () => { if (!email) { setError('Please enter your email address.'); return; } setIsLoading(true); setError(''); try { const response = await apiCall<{success: boolean, token: string}>('password.php', 'POST', { action: 'forgot', email }); setToken(response.token); setIsSent(true); } catch (err) { setError((err as Error).message); } finally { setIsLoading(false); } }; const handleProceed = () => { onSuccess(token); }; return ( {!isSent ? ( <>

Enter your email address and we'll send you a link to reset your password.

setEmail(e.target.value)} />
{error &&

{error}

}
) : ( <>

For demonstration, your password reset token is below. In a real app, a link would be sent to your email.

{token}

Click "Proceed" to continue to the password reset screen.

)}
); }; const ResetPasswordModal: React.FC<{ isOpen: boolean; onClose: () => void; onSuccess: () => void; token: string; }> = ({ isOpen, onClose, onSuccess, token }) => { const [newPassword, setNewPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(''); useEffect(() => { if(isOpen) { setNewPassword(''); setConfirmPassword(''); setIsLoading(false); setError(''); } }, [isOpen]); const handleResetPassword = async () => { if (newPassword !== confirmPassword) { setError("Passwords do not match."); return; } if (newPassword.length < 8) { setError("Password must be at least 8 characters long."); return; } setIsLoading(true); setError(''); try { await apiCall('password.php', 'POST', { action: 'reset', token, newPassword }); onSuccess(); } catch (err) { setError((err as Error).message); } finally { setIsLoading(false); } }; return ( } >
setNewPassword(e.target.value)} />
setConfirmPassword(e.target.value)} />
{error &&

{error}

}
); }; const LoginPage: React.FC<{ onLogin: (user: User) => void }> = ({ onLogin }) => { const [email, setEmail] = useState('admin@zargoona.com'); const [password, setPassword] = useState('password123'); const [error, setError] = useState(''); const [isLoading, setIsLoading] = useState(false); const [isForgotModalOpen, setIsForgotModalOpen] = useState(false); const [isResetModalOpen, setIsResetModalOpen] = useState(false); const [resetToken, setResetToken] = useState(''); const [resetSuccessMessage, setResetSuccessMessage] = useState(''); const handleLogin = async (e: React.FormEvent) => { e.preventDefault(); setIsLoading(true); setError(''); try { const user = await apiCall('login.php', 'POST', { email, password }); onLogin(user); } catch (err) { setError((err as Error).message); setIsLoading(false); } }; const handleForgotSuccess = (token: string) => { setResetToken(token); setIsForgotModalOpen(false); setIsResetModalOpen(true); }; const handleResetSuccess = () => { setIsResetModalOpen(false); setResetSuccessMessage('Password has been successfully reset. Please log in.'); // Clear message after a few seconds setTimeout(() => setResetSuccessMessage(''), 5000); }; return (
store

Zargoona

Wholesale Management System

{resetSuccessMessage &&
{resetSuccessMessage}
}
setEmail(e.target.value)} required />
setPassword(e.target.value)} required />
{error &&

{error}

}
setIsForgotModalOpen(false)} onSuccess={handleForgotSuccess} /> setIsResetModalOpen(false)} onSuccess={handleResetSuccess} token={resetToken} />
); }; // --- MAIN APP COMPONENT --- const getInitialTheme = (): Theme => { try { const savedTheme = localStorage.getItem('zargoonaTheme'); if (savedTheme === 'dark' || savedTheme === 'light') { return savedTheme; } } catch (error) { console.error("Could not access localStorage to get theme.", error); } return 'light'; }; const App: React.FC = () => { const [theme, setTheme] = useState(getInitialTheme); const [currentPage, setCurrentPage] = useState('dashboard'); const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const [isMobile, setIsMobile] = useState(window.innerWidth <= 768); // App State from server const [products, setProducts] = useState([]); const [sales, setSales] = useState([]); const [contacts, setContacts] = useState([]); const [purchases, setPurchases] = useState([]); const [orders, setOrders] = useState([]); const [returns, setReturns] = useState([]); const [damages, setDamages] = useState([]); const [auditLog, setAuditLog] = useState([]); const [expenseCategories, setExpenseCategories] = useState([]); const [expenses, setExpenses] = useState([]); const [payments, setPayments] = useState([]); const [users, setUsers] = useState([]); const [currentUser, setCurrentUser] = useState(null); const [isProfileModalOpen, setIsProfileModalOpen] = useState(false); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); // Fetch all initial data from the backend useEffect(() => { // This effect should only run once after a user is logged in. // The login flow will set the currentUser, then we can fetch data. if (currentUser) { const fetchInitialData = async () => { setIsLoading(true); // Show loader while fetching data for the logged-in user try { const data = await apiCall('data.php', 'GET'); setProducts(data.products || []); setSales(data.sales || []); setContacts(data.contacts || []); setPurchases(data.purchases || []); setOrders(data.orders || []); setReturns(data.returns || []); setDamages(data.damages || []); setAuditLog(data.auditLog || []); setExpenseCategories(data.expenseCategories || []); setExpenses(data.expenses || []); setPayments(data.payments || []); setUsers(data.users || []); } catch (err) { console.error("Failed to fetch initial data", err); setError((err as Error).message); } finally { setIsLoading(false); } }; fetchInitialData(); } else { // If there's no user, we are on the login page, so we are not "loading" data. setIsLoading(false); } }, [currentUser]); const handleUpdateCurrentUser = (updatedData: { name: string }) => { if (!currentUser) return; const updatedUser = { ...currentUser, ...updatedData }; const newUsers = users.map(u => u.id === currentUser.id ? updatedUser : u); setUsers(newUsers); setCurrentUser(updatedUser); setIsProfileModalOpen(false); }; const handleSetCurrentPage = (page: Page) => { if (!currentUser || !permissions.canViewPage(currentUser.role, page)) { console.warn(`Access denied for role ${currentUser?.role} to page ${page}.`); setCurrentPage('dashboard'); return; } setCurrentPage(page); }; const handleLogin = (user: User) => { setCurrentUser(user); // Reset to dashboard page on login setCurrentPage('dashboard'); }; const handleLogout = () => { setCurrentUser(null); // Clear all data on logout to ensure fresh data for the next user setProducts([]); setSales([]); setContacts([]); setPurchases([]); setOrders([]); setReturns([]); setDamages([]); setAuditLog([]); setExpenseCategories([]); setExpenses([]); setPayments([]); setUsers([]); }; const renderPage = () => { if (!currentUser) return null; if (!permissions.canViewPage(currentUser.role, currentPage)) { return ; } switch (currentPage) { case 'dashboard': return ; case 'products': return ; case 'sales': return ; case 'purchases': return ; case 'orders': return ; case 'returns': return ; case 'damages': return ; case 'payments': return ; case 'expenses': return ; case 'contacts': return ; case 'reports': return ; case 'users': return ; case 'audit': return ; default: return ; } }; const toggleTheme = () => { setTheme(current => { const newTheme = current === 'light' ? 'dark' : 'light'; try { localStorage.setItem('zargoonaTheme', newTheme); } catch (error) { console.error("Failed to save theme to localStorage", error); } return newTheme; }); }; const toggleSidebar = useCallback(() => { setSidebarCollapsed(prev => !prev); }, []); useEffect(() => { document.documentElement.setAttribute('data-theme', theme); }, [theme]); useEffect(() => { const handleResize = () => { const mobile = window.innerWidth <= 768; setIsMobile(mobile); if (mobile) { setSidebarCollapsed(true); } }; window.addEventListener('resize', handleResize); handleResize(); // initial check return () => window.removeEventListener('resize', handleResize); }, []); if (isLoading && !currentUser) { return (
Loading Zargoona...
); } if (error) { return (
error

Connection Error

Could not connect to the Zargoona backend.

{error}
Please ensure your PHP backend server is running and the API files are in the correct location.
); } if (!currentUser) { return ; } return (
setIsProfileModalOpen(true)} onLogout={handleLogout} />
{isLoading ? (
Loading data...
) : renderPage()}
{currentUser && setIsProfileModalOpen(false)} user={currentUser} onSave={handleUpdateCurrentUser} />}
); }; const container = document.getElementById('root'); if (container) { const root = createRoot(container); root.render(); }