diff --git a/src/components/inventory/AddItemForm.tsx b/src/components/inventory/AddItemForm.tsx index a16782f..d0e0248 100644 --- a/src/components/inventory/AddItemForm.tsx +++ b/src/components/inventory/AddItemForm.tsx @@ -59,6 +59,9 @@ const statusMessages = { export const AddItemForm = ({ onAddItem, onCancel, existingSkus }: AddItemFormProps) => { + // Convert to Set for O(1) lookup performance + const existingSkusSet = new Set(existingSkus); + const [formData, setFormData] = useState>({ name: '', price: 0, @@ -85,7 +88,7 @@ export const AddItemForm = ({ onAddItem, onCancel, existingSkus }: AddItemFormPr const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); - if (existingSkus.includes(formData.sku)) { + if (existingSkusSet.has(formData.sku)) { setSkuError('SKU already exists'); return; } @@ -105,7 +108,7 @@ export const AddItemForm = ({ onAddItem, onCancel, existingSkus }: AddItemFormPr const handleSkuChange = (e: React.ChangeEvent) => { const value = e.target.value; setFormData(prev => ({ ...prev, sku: value })); - if (existingSkus.includes(value)) { + if (existingSkusSet.has(value)) { setSkuError('SKU already exists'); } else { setSkuError(null); diff --git a/src/components/inventory/EditItemForm.tsx b/src/components/inventory/EditItemForm.tsx index a5c28f3..6e8ce9b 100644 --- a/src/components/inventory/EditItemForm.tsx +++ b/src/components/inventory/EditItemForm.tsx @@ -12,11 +12,13 @@ interface EditItemFormProps { } export const EditItemForm = ({ item, onEditItem, onCancel, existingSkus }: EditItemFormProps) => { + // Convert to Set for O(1) lookup performance + const existingSkusSet = new Set(existingSkus); const [skuError, setSkuError] = useState(null); const [category, setCategory] = useState(item.category || ''); const validateSku = (sku: string): boolean => { - if (existingSkus.includes(sku)) { + if (existingSkusSet.has(sku)) { setSkuError('SKU already exists. Please enter a unique SKU.'); return false; } diff --git a/src/components/reports/DailySalesReport.tsx b/src/components/reports/DailySalesReport.tsx index 76afbf3..f8a2461 100644 --- a/src/components/reports/DailySalesReport.tsx +++ b/src/components/reports/DailySalesReport.tsx @@ -6,12 +6,20 @@ import { useEffect, useState } from "react"; const DailySalesReport = () => { const [sales, setSales] = useState([]); - const today = new Date().toISOString().split("T")[0]; + const today = new Date(); + today.setHours(0, 0, 0, 0); + const todayTime = today.getTime(); + const tomorrowTime = todayTime + 24 * 60 * 60 * 1000; useEffect(() => { setSales(salesService.getAll()); }, []); - const dailySales = sales.filter((sale) => sale.date.startsWith(today)); + + // Use timestamp comparison for better performance + const dailySales = sales.filter((sale) => { + const saleTime = new Date(sale.date).getTime(); + return saleTime >= todayTime && saleTime < tomorrowTime; + }); const totalSales = dailySales.reduce((total, sale) => total + sale.total, 0); diff --git a/src/components/reports/ExpiringItemsReport.tsx b/src/components/reports/ExpiringItemsReport.tsx index d3c1432..157a3e2 100644 --- a/src/components/reports/ExpiringItemsReport.tsx +++ b/src/components/reports/ExpiringItemsReport.tsx @@ -5,18 +5,20 @@ import { useEffect, useState } from "react"; const ExpiringItemsReport = () => { const [products, setProducts] = useState([]); - const today = new Date(); useEffect(() => { setProducts(inventoryService.getAll()); }, []); - const next30Days = new Date(); - next30Days.setDate(today.getDate() + 30); + + // Calculate dates once for better performance + const todayTime = new Date().getTime(); + const next30DaysTime = todayTime + (30 * 24 * 60 * 60 * 1000); + // Use timestamp comparison for better performance const expiringItems = products.filter((product) => { if (!product.expiryDate) return false; - const expiryDate = new Date(product.expiryDate); - return expiryDate >= today && expiryDate <= next30Days; + const expiryTime = new Date(product.expiryDate).getTime(); + return expiryTime >= todayTime && expiryTime <= next30DaysTime; }); return ( diff --git a/src/components/reports/MonthlySalesReport.tsx b/src/components/reports/MonthlySalesReport.tsx index e997ca5..89420fc 100644 --- a/src/components/reports/MonthlySalesReport.tsx +++ b/src/components/reports/MonthlySalesReport.tsx @@ -19,11 +19,17 @@ interface Sale { const MonthlySalesReport = () => { const [sales, setSales] = useState([]); const currentMonth = new Date().getMonth(); + const currentYear = new Date().getFullYear(); useEffect(() => { setSales(salesService.getAll()); }, []); - const monthlySales = sales.filter((sale) => new Date(sale.date).getMonth() === currentMonth); + + // Optimize by checking both month and year for accuracy + const monthlySales = sales.filter((sale) => { + const saleDate = new Date(sale.date); + return saleDate.getMonth() === currentMonth && saleDate.getFullYear() === currentYear; + }); const totalSales = monthlySales.reduce((total, sale) => total + sale.total, 0); diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx index d982c53..fdd73f6 100644 --- a/src/context/AuthContext.tsx +++ b/src/context/AuthContext.tsx @@ -1,4 +1,4 @@ -import React, { createContext, useState, useEffect } from 'react'; +import React, { createContext, useState, useEffect, useCallback } from 'react'; import type { ReactNode } from 'react'; import { useNavigate } from 'react-router-dom'; import { userService } from '@/services/storage'; @@ -35,14 +35,15 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { setLoading(false); }, []); - const login = async (username: string, password: string, role: UserRole) => { + const login = useCallback(async (username: string, password: string, role: UserRole) => { try { const user = userService.validateCredentials(username, password); // Check if user exists and has the correct role if (user && user.role === role) { - // Remove password before storing in state - const { password: _, ...userData } = user; + // Remove password before storing in state for security + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { password: unusedPassword, ...userData } = user; // Remove password from user data localStorage.setItem('currentUser', JSON.stringify(userData)); setUser(userData); return true; @@ -58,13 +59,13 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { console.error('Login error:', error); return false; } - }; + }, []); - const logout = () => { + const logout = useCallback(() => { localStorage.removeItem('currentUser'); setUser(null); navigate('/login'); - }; + }, [navigate]); return ( diff --git a/src/pages/admin/Dashboard.tsx b/src/pages/admin/Dashboard.tsx index 5ea9fff..d3765d4 100644 --- a/src/pages/admin/Dashboard.tsx +++ b/src/pages/admin/Dashboard.tsx @@ -80,79 +80,87 @@ const Dashboard = () => { // Calculate metrics const metrics = useMemo(() => { - const today = new Date(); - const yesterday = new Date(today); - yesterday.setDate(yesterday.getDate() - 1); + const todayStart = new Date(); + todayStart.setHours(0, 0, 0, 0); + const todayStartTime = todayStart.getTime(); - const todaySales = sales.filter(sale => { - const saleDate = new Date(sale.date); - return saleDate.toDateString() === today.toDateString(); - }); + const yesterdayStart = new Date(todayStart); + yesterdayStart.setDate(yesterdayStart.getDate() - 1); + const yesterdayStartTime = yesterdayStart.getTime(); + const yesterdayEndTime = todayStartTime; - const yesterdaySales = sales.filter(sale => { - const saleDate = new Date(sale.date); - return saleDate.toDateString() === yesterday.toDateString(); - }); + const weekAgoTime = todayStartTime - (7 * 24 * 60 * 60 * 1000); + const monthAgoTime = todayStartTime - (30 * 24 * 60 * 60 * 1000); + + // Single pass through sales for all time-based calculations + let totalTodaySales = 0; + let totalYesterdaySales = 0; + let totalWeeklySales = 0; + let totalMonthlySales = 0; + let totalAllSales = 0; + + for (const sale of sales) { + const saleTime = new Date(sale.date).getTime(); + totalAllSales += sale.total; + + if (saleTime >= todayStartTime) { + totalTodaySales += sale.total; + totalWeeklySales += sale.total; + totalMonthlySales += sale.total; + } else if (saleTime >= yesterdayStartTime && saleTime < yesterdayEndTime) { + totalYesterdaySales += sale.total; + totalWeeklySales += sale.total; + totalMonthlySales += sale.total; + } else if (saleTime >= weekAgoTime) { + totalWeeklySales += sale.total; + totalMonthlySales += sale.total; + } else if (saleTime >= monthAgoTime) { + totalMonthlySales += sale.total; + } + } - const totalTodaySales = todaySales.reduce((sum, sale) => sum + sale.total, 0); - const totalYesterdaySales = yesterdaySales.reduce((sum, sale) => sum + sale.total, 0); const salesChange = totalYesterdaySales > 0 ? ((totalTodaySales - totalYesterdaySales) / totalYesterdaySales) * 100 : 0; - - // Calculate weekly sales (last 7 days) - const oneWeekAgo = new Date(today); - oneWeekAgo.setDate(oneWeekAgo.getDate() - 7); - const weeklySales = sales.filter(sale => { - const saleDate = new Date(sale.date); - return saleDate >= oneWeekAgo; - }); - const totalWeeklySales = weeklySales.reduce((sum, sale) => sum + sale.total, 0); - // Calculate monthly sales (last 30 days) - const oneMonthAgo = new Date(today); - oneMonthAgo.setDate(oneMonthAgo.getDate() - 30); - const monthlySales = sales.filter(sale => { - const saleDate = new Date(sale.date); - return saleDate >= oneMonthAgo; - }); - const totalMonthlySales = monthlySales.reduce((sum, sale) => sum + sale.total, 0); + // Single pass through inventory for all calculations + let criticalItemsCount = 0; + let totalStock = 0; + let totalInventoryValue = 0; + let lowStockItemsCount = 0; + let lowStockValue = 0; + const inventoryByCategory: Record = {}; - const criticalItems = inventory.filter(item => - item.quantity <= (item.criticalLevel || 5) - ); - - // Calculate total stock across all items - const totalStock = inventory.reduce((sum, item) => sum + item.quantity, 0); - - // Calculate price-related metrics - const totalInventoryValue = inventory.reduce((sum, item) => { + for (const item of inventory) { + const quantity = item.quantity; + // eslint-disable-next-line @typescript-eslint/no-explicit-any const price = (item as any).price || 0; - return sum + (price * item.quantity); - }, 0); - - const avgProductPrice = inventory.length > 0 - ? totalInventoryValue / inventory.reduce((sum, item) => sum + item.quantity, 0) - : 0; - - const lowStockValue = inventory - .filter(item => item.quantity < 10) - .reduce((sum, item) => { - const price = (item as any).price || 0; - return sum + (price * item.quantity); - }, 0); - - // Group inventory by category for the pie chart - const inventoryByCategory = inventory.reduce>((acc, item) => { + const itemValue = price * quantity; + + totalStock += quantity; + totalInventoryValue += itemValue; + + if (quantity <= (item.criticalLevel || 5)) { + criticalItemsCount++; + } + + if (quantity < 10) { + lowStockItemsCount++; + lowStockValue += itemValue; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any const category = (item as any).category || 'Uncategorized'; - const price = (item as any).price || 0; - if (!acc[category]) { - acc[category] = { count: 0, value: 0 }; + if (!inventoryByCategory[category]) { + inventoryByCategory[category] = { count: 0, value: 0 }; } - acc[category].count += item.quantity; - acc[category].value += price * item.quantity; - return acc; - }, {}); + inventoryByCategory[category].count += quantity; + inventoryByCategory[category].value += itemValue; + } + + const avgProductPrice = totalStock > 0 + ? totalInventoryValue / totalStock + : 0; // Convert to array for the pie chart const pieChartData = Object.entries(inventoryByCategory).map(([name, data]) => ({ @@ -162,7 +170,7 @@ const Dashboard = () => { })); return { - totalSales: sales.reduce((sum, sale) => sum + sale.total, 0), + totalSales: totalAllSales, weeklySales: totalWeeklySales, monthlySales: totalMonthlySales, totalProducts: inventory.length, @@ -170,8 +178,8 @@ const Dashboard = () => { totalInventoryValue, avgProductPrice, lowStockValue, - lowStockItems: inventory.filter(item => item.quantity < 10).length, - criticalItems: criticalItems.length, + lowStockItems: lowStockItemsCount, + criticalItems: criticalItemsCount, todaySales: totalTodaySales, salesChange, pieChartData diff --git a/src/pages/admin/Inventory.tsx b/src/pages/admin/Inventory.tsx index a0d50e3..d54738d 100644 --- a/src/pages/admin/Inventory.tsx +++ b/src/pages/admin/Inventory.tsx @@ -19,7 +19,7 @@ const Inventory = () => { const [showAddItemForm, setShowAddItemForm] = useState(false); const [editingItem, setEditingItem] = useState(null); - // Get unique categories from inventory items + // Get unique categories from inventory items const categories = useMemo(() => { const categorySet = new Set(); inventoryItems.forEach(item => { @@ -29,6 +29,11 @@ const Inventory = () => { }); return Array.from(categorySet).sort(); }, [inventoryItems]); + + // Create SKU set for fast validation + const existingSkusSet = useMemo(() => { + return new Set(inventoryItems.map(item => item.sku).filter(Boolean)); + }, [inventoryItems]); // Load inventory items on component mount useEffect(() => { @@ -157,7 +162,7 @@ const Inventory = () => { setShowAddItemForm(false)} - existingSkus={inventoryItems.map(item => item.sku).filter(Boolean) as string[]} + existingSkus={Array.from(existingSkusSet)} /> @@ -173,10 +178,7 @@ const Inventory = () => { item={editingItem} onEditItem={handleUpdateItem} onCancel={() => setEditingItem(null)} - existingSkus={inventoryItems - .filter(item => item.id !== editingItem.id) // Exclude current item's SKU - .map(item => item.sku) - .filter(Boolean) as string[]} + existingSkus={Array.from(existingSkusSet).filter(sku => sku !== editingItem.sku)} /> diff --git a/src/pages/admin/NewSale.tsx b/src/pages/admin/NewSale.tsx index 7f40fa1..8adc0d2 100644 --- a/src/pages/admin/NewSale.tsx +++ b/src/pages/admin/NewSale.tsx @@ -89,9 +89,12 @@ const NewSale: React.FC = () => { if (cart.length === 0) return; try { + // Create a product map for O(1) lookups instead of O(n) for each item + const productMap = new Map(products.map(p => [p.id, p])); + // First, validate all items have sufficient stock for (const item of cart) { - const product = products.find(p => p.id === item.productId); + const product = productMap.get(item.productId); if (!product) { throw new Error(`Product ${item.name} not found`); } @@ -102,7 +105,7 @@ const NewSale: React.FC = () => { // Process inventory updates for (const item of cart) { - const product = products.find(p => p.id === item.productId)!; + const product = productMap.get(item.productId)!; const newQuantity = product.quantity - item.quantity; await inventoryService.update(product.id, { diff --git a/src/services/storage.ts b/src/services/storage.ts index a680b95..ad27a09 100644 --- a/src/services/storage.ts +++ b/src/services/storage.ts @@ -41,11 +41,31 @@ const initializeStorage = () => { // Initialize storage when the module is loaded initializeStorage(); -// Generic storage functions -const getItems = (key: string): T[] => { +// Cache for frequently accessed data +const cache = new Map(); +const CACHE_TTL = 5000; // 5 seconds cache TTL for better cache effectiveness + +// Generic storage functions with caching +const getItems = (key: string, useCache = true): T[] => { try { + // Check cache first + if (useCache) { + const cached = cache.get(key); + if (cached && Date.now() - cached.timestamp < CACHE_TTL) { + // Return a shallow copy to prevent mutations affecting cached data + return [...(cached.data as T[])]; + } + } + const data = localStorage.getItem(key); - return data ? JSON.parse(data) : []; + const items = data ? JSON.parse(data) : []; + + // Update cache with a copy + if (useCache) { + cache.set(key, { data: [...items], timestamp: Date.now() }); + } + + return items; } catch (error) { console.error(`Error getting items from ${key}:`, error); return []; @@ -55,6 +75,8 @@ const getItems = (key: string): T[] => { const saveItems = (key: string, items: T[]): void => { try { localStorage.setItem(key, JSON.stringify(items)); + // Invalidate cache on save + cache.delete(key); } catch (error) { console.error(`Error saving items to ${key}:`, error); } @@ -72,7 +94,8 @@ export const inventoryService = { create: (item: Omit): Product => { const items = getItems(STORAGE_KEYS.INVENTORY); const newItem = { ...item, id: Date.now().toString() }; - saveItems(STORAGE_KEYS.INVENTORY, [...items, newItem]); + items.push(newItem); // Push instead of spread for better performance + saveItems(STORAGE_KEYS.INVENTORY, items); return newItem; }, @@ -82,12 +105,11 @@ export const inventoryService = { if (index === -1) return undefined; - const updatedItem = { ...items[index], ...updates }; - const updatedItems = [...items]; - updatedItems[index] = updatedItem; + // Update in place instead of creating new array + items[index] = { ...items[index], ...updates }; - saveItems(STORAGE_KEYS.INVENTORY, updatedItems); - return updatedItem; + saveItems(STORAGE_KEYS.INVENTORY, items); + return items[index]; }, delete: (id: string): boolean => { @@ -112,15 +134,20 @@ export const salesService = { id: Date.now().toString(), date: new Date().toISOString() }; - saveItems(STORAGE_KEYS.SALES, [...sales, newSale]); + sales.push(newSale); // Push instead of spread + saveItems(STORAGE_KEYS.SALES, sales); return newSale; }, getByDateRange: (startDate: Date, endDate: Date): Sale[] => { const sales = getItems(STORAGE_KEYS.SALES); + const startTime = startDate.getTime(); + const endTime = endDate.getTime(); + + // Use timestamp comparison for better performance return sales.filter(sale => { - const saleDate = new Date(sale.date); - return saleDate >= startDate && saleDate <= endDate; + const saleTime = new Date(sale.date).getTime(); + return saleTime >= startTime && saleTime <= endTime; }); } }; @@ -148,12 +175,11 @@ export const userService = { if (index === -1) return undefined; - const updatedUser = { ...users[index], ...updates }; - const updatedUsers = [...users]; - updatedUsers[index] = updatedUser; + // Update in place + users[index] = { ...users[index], ...updates }; - saveItems(STORAGE_KEYS.USERS, updatedUsers); - return updatedUser; + saveItems(STORAGE_KEYS.USERS, users); + return users[index]; }, createAdmin: (username: string, password: string, name: string): User | { error: string } => { @@ -173,7 +199,8 @@ export const userService = { name }; - saveItems(STORAGE_KEYS.USERS, [...users, newAdmin]); + users.push(newAdmin); // Push instead of spread + saveItems(STORAGE_KEYS.USERS, users); return newAdmin; } }; @@ -225,7 +252,8 @@ export const salaryService = { create: (record: Omit): SalaryRecord => { const records = getItems(STORAGE_KEYS.SALARIES); const newRecord = { ...record, id: Date.now().toString() }; - saveItems(STORAGE_KEYS.SALARIES, [...records, newRecord]); + records.push(newRecord); // Push instead of spread + saveItems(STORAGE_KEYS.SALARIES, records); return newRecord; }, @@ -235,12 +263,11 @@ export const salaryService = { if (index === -1) return undefined; - const updatedRecord = { ...records[index], ...updates }; - const updatedRecords = [...records]; - updatedRecords[index] = updatedRecord; + // Update in place + records[index] = { ...records[index], ...updates }; - saveItems(STORAGE_KEYS.SALARIES, updatedRecords); - return updatedRecord; + saveItems(STORAGE_KEYS.SALARIES, records); + return records[index]; }, delete: (id: string): boolean => {