diff --git a/client/src/components/Layout.tsx b/client/src/components/Layout.tsx
index a21c7e9..621f4d2 100644
--- a/client/src/components/Layout.tsx
+++ b/client/src/components/Layout.tsx
@@ -1,7 +1,60 @@
-import React from 'react';
-import { Outlet } from 'react-router-dom';
+import React, { useEffect } from 'react';
+import { Outlet, useLocation } from 'react-router-dom';
+import { useDispatch, useSelector } from 'react-redux';
+import { RootState } from '../store';
+import { logout } from '../store/slices/authSlice';
+import { setProducts } from '../store/slices/productSlice';
+import { setOrders } from '../store/slices/orderSlice';
+import { setExpenses } from '../store/slices/expenseSlice';
+import { setCustomers } from '../store/slices/customerSlice';
+import api from '../utils/api';
const Layout: React.FC = () => {
+ const dispatch = useDispatch();
+ const location = useLocation();
+ const user = useSelector((state: RootState) => state.auth.user);
+
+ useEffect(() => {
+ const fetchAll = async () => {
+ try {
+ const [productsRes, ordersRes, expensesRes, customersRes] = await Promise.all([
+ api.get('/products?limit=1000'),
+ api.get('/orders?limit=1000'),
+ api.get('/expenses?limit=1000'),
+ api.get('/customers?limit=1000'),
+ ]);
+ dispatch(setProducts(productsRes.data.products));
+ dispatch(setOrders(ordersRes.data.orders));
+ dispatch(setExpenses(expensesRes.data.expenses));
+ dispatch(setCustomers(customersRes.data.customers));
+ } catch (err) {
+ console.error('Failed to fetch app data', err);
+ }
+ };
+ fetchAll();
+ }, []);
+
+ const handleLogout = () => {
+ dispatch(logout());
+ window.location.href = '/login';
+ };
+
+ const navLink = (href: string, label: string) => {
+ const active = location.pathname === href || (href !== '/' && location.pathname.startsWith(href));
+ return (
+
+ {label}
+
+ );
+ };
+
return (
+
+ {user && (
+ {user.email}
+ )}
+
+
@@ -42,4 +94,4 @@ const Layout: React.FC = () => {
);
};
-export default Layout;
\ No newline at end of file
+export default Layout;
diff --git a/client/src/components/MissingProductsModal.tsx b/client/src/components/MissingProductsModal.tsx
index a5d8fe9..8011d0f 100644
--- a/client/src/components/MissingProductsModal.tsx
+++ b/client/src/components/MissingProductsModal.tsx
@@ -3,6 +3,7 @@ import { useDispatch } from 'react-redux';
import { addProduct } from '../store/slices/productSlice';
import { X, Plus } from 'lucide-react';
import toast from 'react-hot-toast';
+import api from '../utils/api';
export interface MissingProduct {
title: string;
@@ -39,39 +40,50 @@ export const MissingProductsModal: React.FC = ({
}));
};
- const handleAddProducts = () => {
+ const handleAddProducts = async () => {
const newProducts: any[] = [];
-
+
for (const product of missingProducts) {
const data = productData[product.title] || {};
const printingCost = data.printingCost || 0;
const category = data.category || 'Imported Items';
-
- // Extract size from title if present
- const sizeMatch = product.title.match(/Size:\s*([^,\s]+(?:\s+[^,\s]+)*)/i);
- const extractedSize = sizeMatch ? sizeMatch[1].trim() : '';
-
- const newProduct = {
- _id: `product_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
- title: product.title,
- description: `Imported from order ${product.orderNumber || 'Unknown'}`,
- price: product.price || 0, // Default to 0 if price not available (e.g., from packing slip only)
- costOfGoods: 0, // Set to 0 since printing cost includes materials
- printingCost: printingCost,
- sku: `IMP_${Date.now()}_${newProducts.length + 1}`,
- stockLevel: 0, // Set to 0 since we don't know stock
- category: category,
- size: extractedSize,
- tags: [],
- inventory: { quantity: 0, lowStockAlert: 5 },
- isActive: true
- };
-
- dispatch(addProduct(newProduct));
- newProducts.push(newProduct);
+
+ try {
+ const res = await api.post('/products', {
+ title: product.title,
+ description: `Imported from order ${product.orderNumber || 'Unknown'}`,
+ price: product.price || 0,
+ costOfGoods: 0,
+ printingCost,
+ sku: '',
+ category,
+ tags: [],
+ inventory: { quantity: 0, lowStockAlert: 5 },
+ isActive: true
+ });
+ dispatch(addProduct(res.data));
+ newProducts.push(res.data);
+ } catch {
+ // Fall back to local-only if API fails
+ const fallback = {
+ _id: `product_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
+ title: product.title,
+ description: `Imported from order ${product.orderNumber || 'Unknown'}`,
+ price: product.price || 0,
+ costOfGoods: 0,
+ printingCost,
+ sku: '',
+ category,
+ tags: [],
+ inventory: { quantity: 0, lowStockAlert: 5 },
+ isActive: true
+ };
+ dispatch(addProduct(fallback));
+ newProducts.push(fallback);
+ }
}
-
- toast.success(`Added ${newProducts.length} new products with printing costs (inc. materials)`);
+
+ toast.success(`Added ${newProducts.length} new products with printing costs`);
onComplete(newProducts);
};
diff --git a/client/src/pages/DataImport.tsx b/client/src/pages/DataImport.tsx
index ed032cf..7a876f6 100644
--- a/client/src/pages/DataImport.tsx
+++ b/client/src/pages/DataImport.tsx
@@ -11,6 +11,7 @@ import { Order } from '../store/slices/orderSlice';
import { Upload, FileText, Package, Truck, Trash2 } from 'lucide-react';
import toast from 'react-hot-toast';
import { dateTestResults } from '../utils/testDateParsing';
+import api from '../utils/api';
interface ImportResults {
etsyOrders: ParsedEtsyOrder[];
@@ -153,58 +154,36 @@ export default function DataImport() {
summary
});
- // Automatically create orders from CSV data
+ // Upsert all orders via bulk API endpoint
const csvOrders = etsyOrders.map(csvOrder => {
- // Check if order already exists
const existingOrder = orders.find(order => order.orderNumber === csvOrder.orderNumber);
-
- if (existingOrder) {
- // Update existing order with CSV revenue data
- return {
- ...existingOrder,
- total: csvOrder.saleAmount,
- fees: {
- etsy: csvOrder.totalFees || 0,
- processing: 0,
- shipping: 0
- }
- };
- } else {
- // Create new order from CSV data
- return {
- _id: `csv-${csvOrder.orderNumber}`,
- orderNumber: csvOrder.orderNumber,
- total: csvOrder.saleAmount,
- status: 'delivered' as const,
- dateOrdered: csvOrder.date,
- customer: {
- name: 'Etsy Customer',
- email: ''
- },
- items: [{
- title: csvOrder.productName || 'Product from Etsy',
- quantity: 1,
- price: csvOrder.saleAmount,
- printingCost: 0
- }],
- fees: {
- etsy: csvOrder.totalFees || 0,
- processing: 0,
- shipping: 0
- }
- };
- }
+ return {
+ ...(existingOrder || {}),
+ orderNumber: csvOrder.orderNumber,
+ total: csvOrder.saleAmount,
+ status: 'delivered' as const,
+ paymentStatus: 'paid',
+ dateOrdered: csvOrder.date,
+ customer: existingOrder?.customer || { name: 'Etsy Customer', email: '' },
+ items: existingOrder?.items || [{
+ title: csvOrder.productName || 'Product from Etsy',
+ quantity: 1,
+ price: csvOrder.saleAmount,
+ printingCost: 0
+ }],
+ fees: { etsy: csvOrder.totalFees || 0, processing: 0, shipping: 0 }
+ };
});
- // Update existing orders and add new ones
- const existingOrderNumbers = new Set(orders.map(o => o.orderNumber));
- const ordersToUpdate = csvOrders.filter(o => existingOrderNumbers.has(o.orderNumber));
- const ordersToAdd = csvOrders.filter(o => !existingOrderNumbers.has(o.orderNumber));
-
- ordersToUpdate.forEach(order => dispatch(updateOrder(order)));
- ordersToAdd.forEach(order => dispatch(addOrder(order)));
-
- toast.success(`CSV imported! Created ${ordersToAdd.length} new orders and updated ${ordersToUpdate.length} existing orders.`);
+ try {
+ const res = await api.post('/orders/bulk', csvOrders);
+ // Reload orders from API after bulk upsert
+ const ordersRes = await api.get('/orders?limit=1000');
+ dispatch(setOrders(ordersRes.data.orders));
+ toast.success(`CSV imported! Created ${res.data.created} new orders and updated ${res.data.updated} existing orders.`);
+ } catch {
+ toast.error('Failed to save orders to database');
+ }
} catch (err) {
console.error('Error processing CSV files:', err);
@@ -291,11 +270,11 @@ export default function DataImport() {
}
}
- const orderData: Order = {
- _id: existingOrder?._id || Date.now().toString(),
+ const orderData = {
orderNumber: slip.orderNumber,
- total: csvOrderData?.saleAmount || existingOrder?.total || 0, // Use CSV revenue data if available
+ total: csvOrderData?.saleAmount || existingOrder?.total || 0,
status: existingOrder?.status || 'processing',
+ paymentStatus: 'paid',
dateOrdered: formattedOrderDate,
customer: existingOrder?.customer || {
name: slip.customerName,
@@ -309,10 +288,17 @@ export default function DataImport() {
} : existingOrder?.fees
};
- if (existingOrder) {
- dispatch(updateOrder(orderData));
- } else {
- dispatch(addOrder(orderData));
+ try {
+ if (existingOrder) {
+ const res = await api.put(`/orders/${existingOrder._id}`, orderData);
+ dispatch(updateOrder(res.data));
+ } else {
+ const res = await api.post('/orders', orderData);
+ dispatch(addOrder(res.data));
+ }
+ } catch (err) {
+ console.error('Failed to save order from PDF slip:', err);
+ toast.error(`Failed to save order #${slip.orderNumber}`);
}
};
@@ -337,15 +323,22 @@ export default function DataImport() {
}
};
- const handleClearAllOrders = () => {
+ const handleClearAllOrders = async () => {
const orderCount = orders.length;
- if (window.confirm(`This will delete all ${orderCount} existing orders. You'll need to re-upload your packing slips to get the correct dates. Are you sure?`)) {
+ if (window.confirm(`This will permanently delete all ${orderCount} orders from the database. Are you sure?`)) {
+ let deleted = 0;
+ for (const order of orders) {
+ try {
+ await api.delete(`/orders/${order._id}`);
+ deleted++;
+ } catch {}
+ }
dispatch(setOrders([]));
- toast.success(`All ${orderCount} orders cleared! Now re-upload your packing slips with fixed date parsing.`);
+ toast.success(`Deleted ${deleted} orders. Re-upload your packing slips to restore.`);
}
};
- const createOrdersFromCSV = () => {
+ const createOrdersFromCSV = async () => {
if (!results || !results.etsyOrders || results.etsyOrders.length === 0) {
toast.error('No CSV data available. Please import Etsy CSV first.');
return;
@@ -393,15 +386,14 @@ export default function DataImport() {
}
});
- // Update existing orders and add new ones
- const existingOrderNumbers = new Set(orders.map(o => o.orderNumber));
- const ordersToUpdate = csvOrders.filter(o => existingOrderNumbers.has(o.orderNumber));
- const ordersToAdd = csvOrders.filter(o => !existingOrderNumbers.has(o.orderNumber));
-
- ordersToUpdate.forEach(order => dispatch(updateOrder(order)));
- ordersToAdd.forEach(order => dispatch(addOrder(order)));
-
- toast.success(`Updated ${ordersToUpdate.length} existing orders and created ${ordersToAdd.length} new orders from CSV data.`);
+ try {
+ const res = await api.post('/orders/bulk', csvOrders);
+ const ordersRes = await api.get('/orders?limit=1000');
+ dispatch(setOrders(ordersRes.data.orders));
+ toast.success(`Re-synced: created ${res.data.created}, updated ${res.data.updated} orders.`);
+ } catch {
+ toast.error('Failed to sync orders');
+ }
};
const debugDataState = () => {
diff --git a/client/src/pages/Expenses.tsx b/client/src/pages/Expenses.tsx
index bde5e74..363c4d4 100644
--- a/client/src/pages/Expenses.tsx
+++ b/client/src/pages/Expenses.tsx
@@ -5,6 +5,7 @@ import { addExpenses, addExpense, updateExpense, deleteExpense } from '../store/
import { formatAustralianDate } from '../utils/dateFormatter';
import { Upload, Plus, Search, Edit, Trash2, Receipt, Download, DollarSign } from 'lucide-react';
import toast from 'react-hot-toast';
+import api from '../utils/api';
interface ExpenseFormData {
description: string;
@@ -136,25 +137,28 @@ const Expenses = () => {
if (expenseData.amount > 0) {
importedExpenses.push({
- _id: `temp-${Date.now()}-${i}`,
description: expenseData.description || 'Imported Expense',
amount: expenseData.amount,
- category: expenseData.category || 'Other',
+ category: expenseData.category || 'Shipping & Postage',
date: expenseData.date || new Date().toISOString().split('T')[0],
- taxDeductible: true, // Assume business expenses are tax deductible
+ taxDeductible: true,
vendor: expenseData.vendor || expenseData.description || 'Unknown',
reference: expenseData.reference
});
}
}
- // Simulate API call - replace with actual API call
- console.log('Imported expenses:', importedExpenses);
-
- // Add the imported expenses to the store
- dispatch(addExpenses(importedExpenses));
-
- toast.success(`Successfully imported ${importedExpenses.length} expenses`);
+ let imported = 0;
+ const saved = [];
+ for (const e of importedExpenses) {
+ try {
+ const res = await api.post('/expenses', e);
+ saved.push(res.data);
+ imported++;
+ } catch {}
+ }
+ dispatch(addExpenses(saved));
+ toast.success(`Successfully imported ${imported} expenses`);
} catch (error) {
console.error('Import error:', error);
@@ -220,22 +224,15 @@ const Expenses = () => {
e.preventDefault();
try {
if (editingExpense) {
- // Update existing expense
- dispatch(updateExpense({
- _id: editingExpense,
- ...formData
- }));
+ const res = await api.put(`/expenses/${editingExpense}`, formData);
+ dispatch(updateExpense(res.data));
toast.success('Expense updated successfully!');
setEditingExpense(null);
} else {
- // Add new expense
- dispatch(addExpense({
- _id: `expense-${Date.now()}`,
- ...formData
- }));
+ const res = await api.post('/expenses', formData);
+ dispatch(addExpense(res.data));
toast.success('Expense added successfully!');
}
-
setShowAddForm(false);
setFormData({
description: '',
@@ -263,10 +260,15 @@ const Expenses = () => {
setShowAddForm(true);
};
- const handleDelete = (expenseId: string) => {
+ const handleDelete = async (expenseId: string) => {
if (window.confirm('Are you sure you want to delete this expense?')) {
- dispatch(deleteExpense(expenseId));
- toast.success('Expense deleted successfully!');
+ try {
+ await api.delete(`/expenses/${expenseId}`);
+ dispatch(deleteExpense(expenseId));
+ toast.success('Expense deleted successfully!');
+ } catch {
+ toast.error('Failed to delete expense');
+ }
}
};
diff --git a/client/src/pages/Orders.tsx b/client/src/pages/Orders.tsx
index cc66a5b..aed2eb1 100644
--- a/client/src/pages/Orders.tsx
+++ b/client/src/pages/Orders.tsx
@@ -1,34 +1,19 @@
import { useState, useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { RootState } from '../store';
-import { updateOrder, deleteOrder, addOrder, Order, setOrders } from '../store/slices/orderSlice';
+import { updateOrder, deleteOrder, addOrder, Order } from '../store/slices/orderSlice';
import { addProduct } from '../store/slices/productSlice';
import { calculateOrderPrintingCost, calculateOrderProfit, enrichOrderItemsWithCosts } from '../utils/orderCalculations';
import { formatAustralianDate } from '../utils/dateFormatter';
import { Search, Package, Download, ArrowRight, Edit, Trash2, X, Plus } from 'lucide-react';
import toast from 'react-hot-toast';
+import api from '../utils/api';
const Orders = () => {
const dispatch = useDispatch();
const { orders } = useSelector((state: RootState) => state.orders);
const { products } = useSelector((state: RootState) => state.products);
- // Debug: Monitor order changes
- useEffect(() => {
- // Clean up orders with invalid IDs on component mount
- const cleanupInvalidOrders = () => {
- const validOrders = orders?.filter(order => order._id && order._id !== 'undefined') || [];
- if (validOrders.length !== orders?.length) {
- console.log(`Removing ${(orders?.length || 0) - validOrders.length} orders with invalid IDs`);
- dispatch(setOrders(validOrders));
- toast.success('Cleaned up orders with invalid IDs');
- }
- };
-
- if (orders && orders.length > 0) {
- cleanupInvalidOrders();
- }
- }, []); // Only run on mount
const [searchTerm, setSearchTerm] = useState('');
const [dateRange, setDateRange] = useState('all');
@@ -161,11 +146,16 @@ const Orders = () => {
setShowEditModal(true);
};
- const handleSaveOrder = (updatedOrder: Order) => {
- dispatch(updateOrder(updatedOrder));
+ const handleSaveOrder = async (updatedOrder: Order) => {
+ try {
+ const res = await api.put(`/orders/${updatedOrder._id}`, updatedOrder);
+ dispatch(updateOrder(res.data));
+ toast.success('Order updated successfully');
+ } catch {
+ toast.error('Failed to update order');
+ }
setShowEditModal(false);
setEditingOrder(null);
- toast.success('Order updated successfully');
};
const handleDeleteClick = (orderId: string) => {
@@ -173,17 +163,20 @@ const Orders = () => {
setShowDeleteConfirm(true);
};
- const handleConfirmDelete = () => {
+ const handleConfirmDelete = async () => {
if (deletingOrderId && deletingOrderId !== 'undefined') {
- dispatch(deleteOrder(deletingOrderId));
- setShowDeleteConfirm(false);
- setDeletingOrderId(null);
- toast.success('Order deleted successfully');
+ try {
+ await api.delete(`/orders/${deletingOrderId}`);
+ dispatch(deleteOrder(deletingOrderId));
+ toast.success('Order deleted successfully');
+ } catch {
+ toast.error('Failed to delete order');
+ }
} else {
toast.error('Cannot delete order: Invalid order ID');
- setShowDeleteConfirm(false);
- setDeletingOrderId(null);
}
+ setShowDeleteConfirm(false);
+ setDeletingOrderId(null);
};
const handleCancelDelete = () => {
@@ -202,29 +195,30 @@ const Orders = () => {
setShowNewProductModal(true);
};
- const handleSaveNewProduct = () => {
+ const handleSaveNewProduct = async () => {
if (!newProductData.title) {
toast.error('Please enter a product title');
return;
}
-
- const newProduct = {
- _id: `product-${Date.now()}`,
- title: newProductData.title,
- description: '',
- price: newProductData.price,
- costOfGoods: newProductData.costOfGoods,
- printingCost: newProductData.printingCost,
- sku: '',
- category: '',
- tags: [],
- inventory: { quantity: 0, lowStockAlert: 10 },
- isActive: true
- };
-
- dispatch(addProduct(newProduct));
- setShowNewProductModal(false);
- toast.success('Product created successfully');
+ try {
+ const res = await api.post('/products', {
+ title: newProductData.title,
+ description: '',
+ price: newProductData.price,
+ costOfGoods: newProductData.costOfGoods,
+ printingCost: newProductData.printingCost,
+ sku: '',
+ category: 'Other',
+ tags: [],
+ inventory: { quantity: 0, lowStockAlert: 10 },
+ isActive: true
+ });
+ dispatch(addProduct(res.data));
+ setShowNewProductModal(false);
+ toast.success('Product created successfully');
+ } catch {
+ toast.error('Failed to create product');
+ }
};
// Manual order handlers
@@ -239,15 +233,13 @@ const Orders = () => {
setShowAddModal(true);
};
- const handleSaveNewOrder = () => {
+ const handleSaveNewOrder = async () => {
if (!newOrder.orderNumber || !newOrder.items[0]?.title) {
toast.error('Please fill in order number and at least one item');
return;
}
const calculatedTotal = newOrder.items.reduce((sum, item) => sum + (item.quantity * item.price), 0);
-
- // Enrich items with printing costs from product database
const enrichedItems = newOrder.items
.filter(item => item.title.trim() !== '')
.map(item => {
@@ -259,19 +251,22 @@ const Orders = () => {
};
});
- const orderToSave: Order = {
- _id: `fb-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, // More unique ID
- orderNumber: `FB-${newOrder.orderNumber}`, // Prefix to identify Facebook orders
- customer: newOrder.customer,
- dateOrdered: newOrder.dateOrdered,
- total: calculatedTotal,
- status: 'delivered' as const,
- items: enrichedItems
- };
-
- dispatch(addOrder(orderToSave));
- setShowAddModal(false);
- toast.success('Facebook Marketplace order added successfully');
+ try {
+ const res = await api.post('/orders', {
+ orderNumber: `FB-${newOrder.orderNumber}`,
+ customer: newOrder.customer,
+ dateOrdered: newOrder.dateOrdered,
+ total: calculatedTotal,
+ status: 'delivered',
+ paymentStatus: 'paid',
+ items: enrichedItems
+ });
+ dispatch(addOrder(res.data));
+ setShowAddModal(false);
+ toast.success('Facebook Marketplace order added successfully');
+ } catch {
+ toast.error('Failed to save order');
+ }
};
const handleAddItem = () => {
diff --git a/client/src/pages/Products.tsx b/client/src/pages/Products.tsx
index 6dde56e..dfcf388 100644
--- a/client/src/pages/Products.tsx
+++ b/client/src/pages/Products.tsx
@@ -1,9 +1,10 @@
import React, { useState, useRef } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { RootState } from '../store';
-import { updateProduct, deleteProduct } from '../store/slices/productSlice';
+import { addProduct, updateProduct, deleteProduct } from '../store/slices/productSlice';
import { Upload, Plus, Search, Edit, Trash2, Package } from 'lucide-react';
import toast from 'react-hot-toast';
+import api from '../utils/api';
interface ProductFormData {
title: string;
@@ -68,12 +69,12 @@ const Products = () => {
const importedProducts = [];
for (let i = 1; i < lines.length; i++) {
if (lines[i].trim() === '') continue;
-
+
const values = lines[i].split(',').map(v => v.trim().replace(/"/g, ''));
const productData: any = {
- printingCost: 0 // Default printing cost
+ printingCost: 0
};
-
+
headers.forEach((header, index) => {
const value = values[index] || '';
switch (header.toLowerCase()) {
@@ -119,19 +120,22 @@ const Products = () => {
if (productData.title) {
importedProducts.push({
...productData,
- inventory: {
- quantity: productData.quantity || 0,
- lowStockAlert: 5
- },
+ inventory: { quantity: productData.quantity || 0, lowStockAlert: 5 },
tags: productData.tags || [],
isActive: true
});
}
}
- // Simulate API call - replace with actual API call
- console.log('Imported products:', importedProducts);
- toast.success(`Successfully imported ${importedProducts.length} products`);
+ let imported = 0;
+ for (const p of importedProducts) {
+ try {
+ const res = await api.post('/products', p);
+ dispatch(addProduct(res.data));
+ imported++;
+ } catch {}
+ }
+ toast.success(`Successfully imported ${imported} products`);
} catch (error) {
console.error('Import error:', error);
@@ -157,15 +161,13 @@ const Products = () => {
const handleAddProduct = async () => {
try {
- // Simulate API call - replace with actual API call
- const newProduct = {
+ const payload = {
...formData,
- _id: `temp-${Date.now()}`,
tags: formData.tags.split(',').map(t => t.trim()).filter(t => t),
isActive: true
};
-
- console.log('New product:', newProduct);
+ const res = await api.post('/products', payload);
+ dispatch(addProduct(res.data));
toast.success('Product added successfully');
setShowAddForm(false);
setFormData({
@@ -208,16 +210,15 @@ const Products = () => {
const handleUpdateProduct = async () => {
if (!editingProduct) return;
-
+
try {
- const updatedProduct = {
- _id: editingProduct,
+ const payload = {
...formData,
tags: formData.tags.split(',').map(t => t.trim()).filter(t => t),
isActive: true
};
-
- dispatch(updateProduct(updatedProduct));
+ const res = await api.put(`/products/${editingProduct}`, payload);
+ dispatch(updateProduct(res.data));
toast.success('Product updated successfully');
setShowAddForm(false);
setEditingProduct(null);
@@ -241,10 +242,15 @@ const Products = () => {
setShowDeleteConfirm(productId);
};
- const confirmDeleteProduct = () => {
+ const confirmDeleteProduct = async () => {
if (showDeleteConfirm) {
- dispatch(deleteProduct(showDeleteConfirm));
- toast.success('Product deleted successfully');
+ try {
+ await api.delete(`/products/${showDeleteConfirm}`);
+ dispatch(deleteProduct(showDeleteConfirm));
+ toast.success('Product deleted successfully');
+ } catch {
+ toast.error('Failed to delete product');
+ }
setShowDeleteConfirm(null);
}
};
diff --git a/client/src/store/slices/customerSlice.ts b/client/src/store/slices/customerSlice.ts
index e7a8c7f..dfa6fc6 100644
--- a/client/src/store/slices/customerSlice.ts
+++ b/client/src/store/slices/customerSlice.ts
@@ -15,7 +15,7 @@ interface CustomerState {
}
const initialState: CustomerState = {
- customers: JSON.parse(localStorage.getItem('customers') || '[]'),
+ customers: [],
loading: false,
error: null,
};
@@ -26,18 +26,13 @@ const customerSlice = createSlice({
reducers: {
setCustomers: (state, action: PayloadAction) => {
state.customers = action.payload;
- localStorage.setItem('customers', JSON.stringify(state.customers));
},
addCustomer: (state, action: PayloadAction) => {
state.customers.push(action.payload);
- localStorage.setItem('customers', JSON.stringify(state.customers));
},
updateCustomer: (state, action: PayloadAction) => {
const index = state.customers.findIndex(c => c._id === action.payload._id);
- if (index !== -1) {
- state.customers[index] = action.payload;
- localStorage.setItem('customers', JSON.stringify(state.customers));
- }
+ if (index !== -1) state.customers[index] = action.payload;
},
setLoading: (state, action: PayloadAction) => {
state.loading = action.payload;
@@ -49,4 +44,4 @@ const customerSlice = createSlice({
});
export const { setCustomers, addCustomer, updateCustomer, setLoading, setError } = customerSlice.actions;
-export default customerSlice.reducer;
\ No newline at end of file
+export default customerSlice.reducer;
diff --git a/client/src/store/slices/expenseSlice.ts b/client/src/store/slices/expenseSlice.ts
index 56d0c78..27b7b27 100644
--- a/client/src/store/slices/expenseSlice.ts
+++ b/client/src/store/slices/expenseSlice.ts
@@ -18,7 +18,7 @@ interface ExpenseState {
}
const initialState: ExpenseState = {
- expenses: JSON.parse(localStorage.getItem('expenses') || '[]'),
+ expenses: [],
loading: false,
error: null,
};
@@ -29,26 +29,19 @@ const expenseSlice = createSlice({
reducers: {
setExpenses: (state, action: PayloadAction) => {
state.expenses = action.payload;
- localStorage.setItem('expenses', JSON.stringify(state.expenses));
},
addExpenses: (state, action: PayloadAction) => {
state.expenses.push(...action.payload);
- localStorage.setItem('expenses', JSON.stringify(state.expenses));
},
addExpense: (state, action: PayloadAction) => {
state.expenses.push(action.payload);
- localStorage.setItem('expenses', JSON.stringify(state.expenses));
},
updateExpense: (state, action: PayloadAction) => {
const index = state.expenses.findIndex(e => e._id === action.payload._id);
- if (index !== -1) {
- state.expenses[index] = action.payload;
- localStorage.setItem('expenses', JSON.stringify(state.expenses));
- }
+ if (index !== -1) state.expenses[index] = action.payload;
},
deleteExpense: (state, action: PayloadAction) => {
state.expenses = state.expenses.filter(e => e._id !== action.payload);
- localStorage.setItem('expenses', JSON.stringify(state.expenses));
},
setLoading: (state, action: PayloadAction) => {
state.loading = action.payload;
@@ -60,4 +53,4 @@ const expenseSlice = createSlice({
});
export const { setExpenses, addExpenses, addExpense, updateExpense, deleteExpense, setLoading, setError } = expenseSlice.actions;
-export default expenseSlice.reducer;
\ No newline at end of file
+export default expenseSlice.reducer;
diff --git a/client/src/store/slices/orderSlice.ts b/client/src/store/slices/orderSlice.ts
index 507048c..b583f1c 100644
--- a/client/src/store/slices/orderSlice.ts
+++ b/client/src/store/slices/orderSlice.ts
@@ -38,7 +38,7 @@ interface OrderState {
}
const initialState: OrderState = {
- orders: JSON.parse(localStorage.getItem('orders') || '[]'),
+ orders: [],
loading: false,
error: null,
};
@@ -49,26 +49,19 @@ const orderSlice = createSlice({
reducers: {
setOrders: (state, action: PayloadAction) => {
state.orders = action.payload;
- localStorage.setItem('orders', JSON.stringify(state.orders));
},
addOrder: (state, action: PayloadAction) => {
state.orders.push(action.payload);
- localStorage.setItem('orders', JSON.stringify(state.orders));
},
addOrders: (state, action: PayloadAction) => {
state.orders.push(...action.payload);
- localStorage.setItem('orders', JSON.stringify(state.orders));
},
updateOrder: (state, action: PayloadAction) => {
const index = state.orders.findIndex(o => o._id === action.payload._id);
- if (index !== -1) {
- state.orders[index] = action.payload;
- localStorage.setItem('orders', JSON.stringify(state.orders));
- }
+ if (index !== -1) state.orders[index] = action.payload;
},
deleteOrder: (state, action: PayloadAction) => {
state.orders = state.orders.filter(o => o._id !== action.payload);
- localStorage.setItem('orders', JSON.stringify(state.orders));
},
setLoading: (state, action: PayloadAction) => {
state.loading = action.payload;
@@ -80,4 +73,4 @@ const orderSlice = createSlice({
});
export const { setOrders, addOrder, addOrders, updateOrder, deleteOrder, setLoading, setError } = orderSlice.actions;
-export default orderSlice.reducer;
\ No newline at end of file
+export default orderSlice.reducer;
diff --git a/client/src/store/slices/productSlice.ts b/client/src/store/slices/productSlice.ts
index 2cc44c2..53bc0ce 100644
--- a/client/src/store/slices/productSlice.ts
+++ b/client/src/store/slices/productSlice.ts
@@ -24,7 +24,7 @@ interface ProductState {
}
const initialState: ProductState = {
- products: JSON.parse(localStorage.getItem('products') || '[]'),
+ products: [],
loading: false,
error: null,
};
@@ -35,22 +35,16 @@ const productSlice = createSlice({
reducers: {
setProducts: (state, action: PayloadAction) => {
state.products = action.payload;
- localStorage.setItem('products', JSON.stringify(state.products));
},
addProduct: (state, action: PayloadAction) => {
state.products.push(action.payload);
- localStorage.setItem('products', JSON.stringify(state.products));
},
updateProduct: (state, action: PayloadAction) => {
const index = state.products.findIndex(p => p._id === action.payload._id);
- if (index !== -1) {
- state.products[index] = action.payload;
- localStorage.setItem('products', JSON.stringify(state.products));
- }
+ if (index !== -1) state.products[index] = action.payload;
},
deleteProduct: (state, action: PayloadAction) => {
state.products = state.products.filter(p => p._id !== action.payload);
- localStorage.setItem('products', JSON.stringify(state.products));
},
setLoading: (state, action: PayloadAction) => {
state.loading = action.payload;
@@ -62,4 +56,4 @@ const productSlice = createSlice({
});
export const { setProducts, addProduct, updateProduct, deleteProduct, setLoading, setError } = productSlice.actions;
-export default productSlice.reducer;
\ No newline at end of file
+export default productSlice.reducer;
diff --git a/server/src/index.ts b/server/src/index.ts
index 02e15f1..419cdd2 100644
--- a/server/src/index.ts
+++ b/server/src/index.ts
@@ -31,8 +31,8 @@ app.use(cors({
// Rate limiting
const limiter = rateLimit({
- windowMs: 15 * 60 * 1000, // 15 minutes
- max: 100, // limit each IP to 100 requests per windowMs
+ windowMs: 15 * 60 * 1000,
+ max: 1000,
message: 'Too many requests from this IP, please try again later.',
});
app.use(limiter);
diff --git a/server/src/models/Expense.ts b/server/src/models/Expense.ts
index 4a95142..31c0dfa 100644
--- a/server/src/models/Expense.ts
+++ b/server/src/models/Expense.ts
@@ -1,10 +1,13 @@
import mongoose, { Document, Schema } from 'mongoose';
export interface IExpense extends Document {
- category: 'materials' | 'shipping' | 'fees' | 'advertising' | 'tools' | 'other';
+ category: string;
description: string;
amount: number;
date: Date;
+ taxDeductible: boolean;
+ vendor?: string;
+ reference?: string;
receiptUrl?: string;
notes?: string;
userId: mongoose.Types.ObjectId;
@@ -13,14 +16,13 @@ export interface IExpense extends Document {
}
const ExpenseSchema: Schema = new Schema({
- category: {
- type: String,
- enum: ['materials', 'shipping', 'fees', 'advertising', 'tools', 'other'],
- required: true,
- },
+ category: { type: String, required: true },
description: { type: String, required: true, trim: true },
amount: { type: Number, required: true, min: 0 },
date: { type: Date, required: true },
+ taxDeductible: { type: Boolean, default: false },
+ vendor: { type: String },
+ reference: { type: String },
receiptUrl: { type: String },
notes: { type: String },
userId: { type: Schema.Types.ObjectId, ref: 'User', required: true, index: true },
diff --git a/server/src/models/Order.ts b/server/src/models/Order.ts
index 66ec14f..d766d67 100644
--- a/server/src/models/Order.ts
+++ b/server/src/models/Order.ts
@@ -3,29 +3,33 @@ import mongoose, { Document, Schema } from 'mongoose';
export interface IOrder extends Document {
orderNumber: string;
etsyOrderId?: string;
- customerId: mongoose.Types.ObjectId;
+ customerId?: mongoose.Types.ObjectId;
+ customer?: { name: string; email: string };
items: {
- productId: mongoose.Types.ObjectId;
- sku: string;
+ productId?: mongoose.Types.ObjectId;
+ sku?: string;
title: string;
quantity: number;
price: number;
+ printingCost?: number;
+ costOfGoods?: number;
variant?: string;
}[];
+ fees?: { etsy: number; processing: number; shipping: number };
subtotal: number;
shipping: number;
tax: number;
total: number;
status: 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled' | 'refunded';
paymentStatus: 'pending' | 'paid' | 'refunded' | 'failed';
- shippingAddress: {
- name: string;
- street1: string;
+ shippingAddress?: {
+ name?: string;
+ street1?: string;
street2?: string;
- city: string;
- state: string;
- zipCode: string;
- country: string;
+ city?: string;
+ state?: string;
+ zipCode?: string;
+ country?: string;
};
trackingNumber?: string;
shippingCarrier?: string;
@@ -39,39 +43,50 @@ export interface IOrder extends Document {
}
const OrderSchema: Schema = new Schema({
- orderNumber: { type: String, required: true, unique: true },
+ orderNumber: { type: String, required: true },
etsyOrderId: { type: String },
- customerId: { type: Schema.Types.ObjectId, ref: 'Customer', required: true },
+ customerId: { type: Schema.Types.ObjectId, ref: 'Customer' },
+ customer: {
+ name: { type: String },
+ email: { type: String },
+ },
items: [{
- productId: { type: Schema.Types.ObjectId, ref: 'Product', required: true },
- sku: { type: String, required: true },
+ productId: { type: Schema.Types.ObjectId, ref: 'Product' },
+ sku: { type: String },
title: { type: String, required: true },
quantity: { type: Number, required: true, min: 1 },
price: { type: Number, required: true, min: 0 },
- variant: { type: String }
+ printingCost: { type: Number, default: 0 },
+ costOfGoods: { type: Number, default: 0 },
+ variant: { type: String },
}],
- subtotal: { type: Number, required: true, min: 0 },
- shipping: { type: Number, required: true, min: 0 },
- tax: { type: Number, required: true, min: 0 },
+ fees: {
+ etsy: { type: Number, default: 0 },
+ processing: { type: Number, default: 0 },
+ shipping: { type: Number, default: 0 },
+ },
+ subtotal: { type: Number, default: 0 },
+ shipping: { type: Number, default: 0 },
+ tax: { type: Number, default: 0 },
total: { type: Number, required: true, min: 0 },
status: {
type: String,
enum: ['pending', 'processing', 'shipped', 'delivered', 'cancelled', 'refunded'],
- default: 'pending'
+ default: 'pending',
},
paymentStatus: {
type: String,
enum: ['pending', 'paid', 'refunded', 'failed'],
- default: 'pending'
+ default: 'paid',
},
shippingAddress: {
- name: { type: String, required: true },
- street1: { type: String, required: true },
+ name: { type: String },
+ street1: { type: String },
street2: { type: String },
- city: { type: String, required: true },
- state: { type: String, required: true },
- zipCode: { type: String, required: true },
- country: { type: String, required: true }
+ city: { type: String },
+ state: { type: String },
+ zipCode: { type: String },
+ country: { type: String },
},
trackingNumber: { type: String },
shippingCarrier: { type: String },
@@ -81,12 +96,14 @@ const OrderSchema: Schema = new Schema({
dateDelivered: { type: Date },
userId: { type: Schema.Types.ObjectId, ref: 'User', required: true, index: true },
dateCreated: { type: Date, default: Date.now },
- dateUpdated: { type: Date, default: Date.now }
+ dateUpdated: { type: Date, default: Date.now },
});
-OrderSchema.pre('save', function(next) {
+OrderSchema.index({ orderNumber: 1, userId: 1 }, { unique: true });
+
+OrderSchema.pre('save', function (next) {
this.dateUpdated = new Date();
next();
});
-export default mongoose.model('Order', OrderSchema);
\ No newline at end of file
+export default mongoose.model('Order', OrderSchema);
diff --git a/server/src/models/Product.ts b/server/src/models/Product.ts
index 02b2997..e0b916e 100644
--- a/server/src/models/Product.ts
+++ b/server/src/models/Product.ts
@@ -5,6 +5,7 @@ export interface IProduct extends Document {
description: string;
price: number;
costOfGoods: number;
+ printingCost: number;
sku: string;
category: string;
tags: string[];
@@ -36,11 +37,12 @@ export interface IProduct extends Document {
const ProductSchema: Schema = new Schema({
title: { type: String, required: true, trim: true },
- description: { type: String, required: true },
+ description: { type: String, default: '' },
price: { type: Number, required: true, min: 0 },
- costOfGoods: { type: Number, required: true, min: 0 },
- sku: { type: String, required: true, unique: true, trim: true },
- category: { type: String, required: true },
+ costOfGoods: { type: Number, default: 0, min: 0 },
+ printingCost: { type: Number, default: 0, min: 0 },
+ sku: { type: String, trim: true, default: '' },
+ category: { type: String, default: 'Other' },
tags: [{ type: String, trim: true }],
images: [{ type: String }],
variants: [{
diff --git a/server/src/routes/orders.ts b/server/src/routes/orders.ts
index ec5ff79..2551d82 100644
--- a/server/src/routes/orders.ts
+++ b/server/src/routes/orders.ts
@@ -7,13 +7,12 @@ const router = Router();
router.get('/', async (req: AuthRequest, res: Response) => {
try {
- const { page = 1, limit = 20, status, paymentStatus } = req.query;
+ const { page = 1, limit = 200, status, paymentStatus } = req.query;
const filter: any = { userId: req.userId };
if (status) filter.status = status;
if (paymentStatus) filter.paymentStatus = paymentStatus;
const orders = await Order.find(filter)
- .populate('customerId', 'name email')
.sort({ dateOrdered: -1 })
.limit(Number(limit))
.skip((Number(page) - 1) * Number(limit));
@@ -27,13 +26,16 @@ router.get('/', async (req: AuthRequest, res: Response) => {
router.post('/', async (req: AuthRequest, res: Response) => {
try {
- const order = new Order({ ...req.body, userId: req.userId });
+ const { _id, ...body } = req.body;
+ const order = new Order({ ...body, userId: req.userId });
await order.save();
- await Customer.findOneAndUpdate(
- { _id: order.customerId, userId: req.userId },
- { $inc: { totalOrders: 1, totalSpent: order.total } }
- );
+ if (order.customerId) {
+ await Customer.findOneAndUpdate(
+ { _id: order.customerId, userId: req.userId },
+ { $inc: { totalOrders: 1, totalSpent: order.total } }
+ );
+ }
res.status(201).json(order);
} catch (err) {
@@ -41,10 +43,36 @@ router.post('/', async (req: AuthRequest, res: Response) => {
}
});
+router.post('/bulk', async (req: AuthRequest, res: Response) => {
+ try {
+ const orders: any[] = req.body;
+ const results = { created: 0, updated: 0, errors: 0 };
+
+ for (const orderData of orders) {
+ try {
+ const { _id, ...body } = orderData;
+ const existing = await Order.findOne({ orderNumber: body.orderNumber, userId: req.userId });
+ if (existing) {
+ await Order.findByIdAndUpdate(existing._id, { ...body, userId: req.userId }, { new: true, runValidators: true });
+ results.updated++;
+ } else {
+ await Order.create({ ...body, userId: req.userId });
+ results.created++;
+ }
+ } catch {
+ results.errors++;
+ }
+ }
+
+ res.json(results);
+ } catch (err) {
+ res.status(400).json({ message: 'Failed to bulk import orders', error: err });
+ }
+});
+
router.get('/:id', async (req: AuthRequest, res: Response) => {
try {
- const order = await Order.findOne({ _id: req.params.id, userId: req.userId })
- .populate('customerId', 'name email');
+ const order = await Order.findOne({ _id: req.params.id, userId: req.userId });
if (!order) return res.status(404).json({ message: 'Order not found' });
res.json(order);
} catch (err) {
@@ -54,9 +82,10 @@ router.get('/:id', async (req: AuthRequest, res: Response) => {
router.put('/:id', async (req: AuthRequest, res: Response) => {
try {
+ const { _id, ...body } = req.body;
const order = await Order.findOneAndUpdate(
{ _id: req.params.id, userId: req.userId },
- req.body,
+ body,
{ new: true, runValidators: true }
);
if (!order) return res.status(404).json({ message: 'Order not found' });
@@ -66,4 +95,14 @@ router.put('/:id', async (req: AuthRequest, res: Response) => {
}
});
+router.delete('/:id', async (req: AuthRequest, res: Response) => {
+ try {
+ const order = await Order.findOneAndDelete({ _id: req.params.id, userId: req.userId });
+ if (!order) return res.status(404).json({ message: 'Order not found' });
+ res.json({ message: 'Order deleted' });
+ } catch (err) {
+ res.status(500).json({ message: 'Failed to delete order', error: err });
+ }
+});
+
export default router;