Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/components/inventory/AddItemForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Omit<Product, 'id' | 'lastUpdated' | 'expiryDate'>>({
name: '',
price: 0,
Expand All @@ -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;
}
Expand All @@ -105,7 +108,7 @@ export const AddItemForm = ({ onAddItem, onCancel, existingSkus }: AddItemFormPr
const handleSkuChange = (e: React.ChangeEvent<HTMLInputElement>) => {
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);
Expand Down
4 changes: 3 additions & 1 deletion src/components/inventory/EditItemForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | null>(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;
}
Expand Down
12 changes: 10 additions & 2 deletions src/components/reports/DailySalesReport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,20 @@ import { useEffect, useState } from "react";

const DailySalesReport = () => {
const [sales, setSales] = useState<Sale[]>([]);
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);

Expand Down
12 changes: 7 additions & 5 deletions src/components/reports/ExpiringItemsReport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,20 @@ import { useEffect, useState } from "react";

const ExpiringItemsReport = () => {
const [products, setProducts] = useState<Product[]>([]);
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 (
Expand Down
8 changes: 7 additions & 1 deletion src/components/reports/MonthlySalesReport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,17 @@ interface Sale {
const MonthlySalesReport = () => {
const [sales, setSales] = useState<Sale[]>([]);
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);

Expand Down
15 changes: 8 additions & 7 deletions src/context/AuthContext.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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;
Expand All @@ -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 (
<AuthContext.Provider value={{ user, login, logout, loading }}>
Expand Down
138 changes: 73 additions & 65 deletions src/pages/admin/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, { count: number; value: number }> = {};

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<Record<string, { count: number; value: number }>>((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]) => ({
Expand All @@ -162,16 +170,16 @@ const Dashboard = () => {
}));

return {
totalSales: sales.reduce((sum, sale) => sum + sale.total, 0),
totalSales: totalAllSales,
weeklySales: totalWeeklySales,
monthlySales: totalMonthlySales,
totalProducts: inventory.length,
totalStock,
totalInventoryValue,
avgProductPrice,
lowStockValue,
lowStockItems: inventory.filter(item => item.quantity < 10).length,
criticalItems: criticalItems.length,
lowStockItems: lowStockItemsCount,
criticalItems: criticalItemsCount,
todaySales: totalTodaySales,
salesChange,
pieChartData
Expand Down
14 changes: 8 additions & 6 deletions src/pages/admin/Inventory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const Inventory = () => {
const [showAddItemForm, setShowAddItemForm] = useState(false);
const [editingItem, setEditingItem] = useState<Product | null>(null);

// Get unique categories from inventory items
// Get unique categories from inventory items
const categories = useMemo(() => {
const categorySet = new Set<string>();
inventoryItems.forEach(item => {
Expand All @@ -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(() => {
Expand Down Expand Up @@ -157,7 +162,7 @@ const Inventory = () => {
<AddItemForm
onAddItem={handleAddItem}
onCancel={() => setShowAddItemForm(false)}
existingSkus={inventoryItems.map(item => item.sku).filter(Boolean) as string[]}
existingSkus={Array.from(existingSkusSet)}
/>
</CardContent>
</Card>
Expand All @@ -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)}
/>
</CardContent>
</Card>
Expand Down
7 changes: 5 additions & 2 deletions src/pages/admin/NewSale.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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`);
}
Expand All @@ -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, {
Expand Down
Loading