Migrate frontend from localStorage to MongoDB API

- Remove localStorage from all 4 Redux slices (products, orders, expenses, customers)
- Layout fetches all data from API on mount; adds logout button with active nav highlighting
- Wire API calls in Products, Orders, Expenses pages for all CRUD operations
- DataImport uses POST /orders/bulk for CSV upserts and API for PDF slip orders
- MissingProductsModal creates products via API
- Relax Order model: optional customerId, embedded customer, fees, printingCost on items, default paymentStatus=paid
- Relax Expense model: free-string category, add taxDeductible/vendor/reference fields
- Add printingCost to Product model
- Add POST /orders/bulk endpoint for upsert-by-orderNumber
- Raise rate limit to 1000 req/15min for bulk imports

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
dlawler489 2026-04-22 08:48:05 +10:00
parent 0d42d97d70
commit 1a3bd33be8
15 changed files with 408 additions and 314 deletions

View file

@ -1,7 +1,60 @@
import React from 'react'; import React, { useEffect } from 'react';
import { Outlet } from 'react-router-dom'; 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 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 (
<a
href={href}
className={`inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium ${
active
? 'border-indigo-500 text-gray-900'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
>
{label}
</a>
);
};
return ( return (
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen bg-gray-50">
<nav className="bg-white shadow-sm border-b"> <nav className="bg-white shadow-sm border-b">
@ -12,26 +65,25 @@ const Layout: React.FC = () => {
<h1 className="text-xl font-bold text-gray-900">Etsy Tracker</h1> <h1 className="text-xl font-bold text-gray-900">Etsy Tracker</h1>
</div> </div>
<div className="hidden sm:ml-6 sm:flex sm:space-x-8"> <div className="hidden sm:ml-6 sm:flex sm:space-x-8">
<a href="/analytics" className="border-indigo-500 text-gray-900 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"> {navLink('/analytics', 'Analytics')}
Analytics {navLink('/profit-analysis', 'Profit Analysis')}
</a> {navLink('/products', 'Products')}
<a href="/profit-analysis" className="border-transparent text-gray-500 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"> {navLink('/orders', 'Orders')}
Profit Analysis {navLink('/expenses', 'Expenses')}
</a> {navLink('/data-import', 'Data Import')}
<a href="/products" className="border-transparent text-gray-500 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium">
Products
</a>
<a href="/orders" className="border-transparent text-gray-500 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium">
Orders
</a>
<a href="/expenses" className="border-transparent text-gray-500 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium">
Expenses
</a>
<a href="/data-import" className="border-transparent text-gray-500 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium">
Data Import
</a>
</div> </div>
</div> </div>
<div className="flex items-center gap-4">
{user && (
<span className="text-sm text-gray-600">{user.email}</span>
)}
<button
onClick={handleLogout}
className="text-sm text-gray-500 hover:text-gray-700 px-3 py-1 border border-gray-300 rounded-md hover:bg-gray-50"
>
Logout
</button>
</div>
</div> </div>
</div> </div>
</nav> </nav>
@ -42,4 +94,4 @@ const Layout: React.FC = () => {
); );
}; };
export default Layout; export default Layout;

View file

@ -3,6 +3,7 @@ import { useDispatch } from 'react-redux';
import { addProduct } from '../store/slices/productSlice'; import { addProduct } from '../store/slices/productSlice';
import { X, Plus } from 'lucide-react'; import { X, Plus } from 'lucide-react';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import api from '../utils/api';
export interface MissingProduct { export interface MissingProduct {
title: string; title: string;
@ -39,39 +40,50 @@ export const MissingProductsModal: React.FC<MissingProductsModalProps> = ({
})); }));
}; };
const handleAddProducts = () => { const handleAddProducts = async () => {
const newProducts: any[] = []; const newProducts: any[] = [];
for (const product of missingProducts) { for (const product of missingProducts) {
const data = productData[product.title] || {}; const data = productData[product.title] || {};
const printingCost = data.printingCost || 0; const printingCost = data.printingCost || 0;
const category = data.category || 'Imported Items'; const category = data.category || 'Imported Items';
// Extract size from title if present try {
const sizeMatch = product.title.match(/Size:\s*([^,\s]+(?:\s+[^,\s]+)*)/i); const res = await api.post('/products', {
const extractedSize = sizeMatch ? sizeMatch[1].trim() : ''; title: product.title,
description: `Imported from order ${product.orderNumber || 'Unknown'}`,
const newProduct = { price: product.price || 0,
_id: `product_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, costOfGoods: 0,
title: product.title, printingCost,
description: `Imported from order ${product.orderNumber || 'Unknown'}`, sku: '',
price: product.price || 0, // Default to 0 if price not available (e.g., from packing slip only) category,
costOfGoods: 0, // Set to 0 since printing cost includes materials tags: [],
printingCost: printingCost, inventory: { quantity: 0, lowStockAlert: 5 },
sku: `IMP_${Date.now()}_${newProducts.length + 1}`, isActive: true
stockLevel: 0, // Set to 0 since we don't know stock });
category: category, dispatch(addProduct(res.data));
size: extractedSize, newProducts.push(res.data);
tags: [], } catch {
inventory: { quantity: 0, lowStockAlert: 5 }, // Fall back to local-only if API fails
isActive: true const fallback = {
}; _id: `product_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
title: product.title,
dispatch(addProduct(newProduct)); description: `Imported from order ${product.orderNumber || 'Unknown'}`,
newProducts.push(newProduct); 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); onComplete(newProducts);
}; };

View file

@ -11,6 +11,7 @@ import { Order } from '../store/slices/orderSlice';
import { Upload, FileText, Package, Truck, Trash2 } from 'lucide-react'; import { Upload, FileText, Package, Truck, Trash2 } from 'lucide-react';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { dateTestResults } from '../utils/testDateParsing'; import { dateTestResults } from '../utils/testDateParsing';
import api from '../utils/api';
interface ImportResults { interface ImportResults {
etsyOrders: ParsedEtsyOrder[]; etsyOrders: ParsedEtsyOrder[];
@ -153,58 +154,36 @@ export default function DataImport() {
summary summary
}); });
// Automatically create orders from CSV data // Upsert all orders via bulk API endpoint
const csvOrders = etsyOrders.map(csvOrder => { const csvOrders = etsyOrders.map(csvOrder => {
// Check if order already exists
const existingOrder = orders.find(order => order.orderNumber === csvOrder.orderNumber); const existingOrder = orders.find(order => order.orderNumber === csvOrder.orderNumber);
return {
if (existingOrder) { ...(existingOrder || {}),
// Update existing order with CSV revenue data orderNumber: csvOrder.orderNumber,
return { total: csvOrder.saleAmount,
...existingOrder, status: 'delivered' as const,
total: csvOrder.saleAmount, paymentStatus: 'paid',
fees: { dateOrdered: csvOrder.date,
etsy: csvOrder.totalFees || 0, customer: existingOrder?.customer || { name: 'Etsy Customer', email: '' },
processing: 0, items: existingOrder?.items || [{
shipping: 0 title: csvOrder.productName || 'Product from Etsy',
} quantity: 1,
}; price: csvOrder.saleAmount,
} else { printingCost: 0
// Create new order from CSV data }],
return { fees: { etsy: csvOrder.totalFees || 0, processing: 0, shipping: 0 }
_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
}
};
}
}); });
// Update existing orders and add new ones try {
const existingOrderNumbers = new Set(orders.map(o => o.orderNumber)); const res = await api.post('/orders/bulk', csvOrders);
const ordersToUpdate = csvOrders.filter(o => existingOrderNumbers.has(o.orderNumber)); // Reload orders from API after bulk upsert
const ordersToAdd = csvOrders.filter(o => !existingOrderNumbers.has(o.orderNumber)); const ordersRes = await api.get('/orders?limit=1000');
dispatch(setOrders(ordersRes.data.orders));
ordersToUpdate.forEach(order => dispatch(updateOrder(order))); toast.success(`CSV imported! Created ${res.data.created} new orders and updated ${res.data.updated} existing orders.`);
ordersToAdd.forEach(order => dispatch(addOrder(order))); } catch {
toast.error('Failed to save orders to database');
toast.success(`CSV imported! Created ${ordersToAdd.length} new orders and updated ${ordersToUpdate.length} existing orders.`); }
} catch (err) { } catch (err) {
console.error('Error processing CSV files:', err); console.error('Error processing CSV files:', err);
@ -291,11 +270,11 @@ export default function DataImport() {
} }
} }
const orderData: Order = { const orderData = {
_id: existingOrder?._id || Date.now().toString(),
orderNumber: slip.orderNumber, 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', status: existingOrder?.status || 'processing',
paymentStatus: 'paid',
dateOrdered: formattedOrderDate, dateOrdered: formattedOrderDate,
customer: existingOrder?.customer || { customer: existingOrder?.customer || {
name: slip.customerName, name: slip.customerName,
@ -309,10 +288,17 @@ export default function DataImport() {
} : existingOrder?.fees } : existingOrder?.fees
}; };
if (existingOrder) { try {
dispatch(updateOrder(orderData)); if (existingOrder) {
} else { const res = await api.put(`/orders/${existingOrder._id}`, orderData);
dispatch(addOrder(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; 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([])); 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) { if (!results || !results.etsyOrders || results.etsyOrders.length === 0) {
toast.error('No CSV data available. Please import Etsy CSV first.'); toast.error('No CSV data available. Please import Etsy CSV first.');
return; return;
@ -393,15 +386,14 @@ export default function DataImport() {
} }
}); });
// Update existing orders and add new ones try {
const existingOrderNumbers = new Set(orders.map(o => o.orderNumber)); const res = await api.post('/orders/bulk', csvOrders);
const ordersToUpdate = csvOrders.filter(o => existingOrderNumbers.has(o.orderNumber)); const ordersRes = await api.get('/orders?limit=1000');
const ordersToAdd = csvOrders.filter(o => !existingOrderNumbers.has(o.orderNumber)); dispatch(setOrders(ordersRes.data.orders));
toast.success(`Re-synced: created ${res.data.created}, updated ${res.data.updated} orders.`);
ordersToUpdate.forEach(order => dispatch(updateOrder(order))); } catch {
ordersToAdd.forEach(order => dispatch(addOrder(order))); toast.error('Failed to sync orders');
}
toast.success(`Updated ${ordersToUpdate.length} existing orders and created ${ordersToAdd.length} new orders from CSV data.`);
}; };
const debugDataState = () => { const debugDataState = () => {

View file

@ -5,6 +5,7 @@ import { addExpenses, addExpense, updateExpense, deleteExpense } from '../store/
import { formatAustralianDate } from '../utils/dateFormatter'; import { formatAustralianDate } from '../utils/dateFormatter';
import { Upload, Plus, Search, Edit, Trash2, Receipt, Download, DollarSign } from 'lucide-react'; import { Upload, Plus, Search, Edit, Trash2, Receipt, Download, DollarSign } from 'lucide-react';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import api from '../utils/api';
interface ExpenseFormData { interface ExpenseFormData {
description: string; description: string;
@ -136,25 +137,28 @@ const Expenses = () => {
if (expenseData.amount > 0) { if (expenseData.amount > 0) {
importedExpenses.push({ importedExpenses.push({
_id: `temp-${Date.now()}-${i}`,
description: expenseData.description || 'Imported Expense', description: expenseData.description || 'Imported Expense',
amount: expenseData.amount, amount: expenseData.amount,
category: expenseData.category || 'Other', category: expenseData.category || 'Shipping & Postage',
date: expenseData.date || new Date().toISOString().split('T')[0], 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', vendor: expenseData.vendor || expenseData.description || 'Unknown',
reference: expenseData.reference reference: expenseData.reference
}); });
} }
} }
// Simulate API call - replace with actual API call let imported = 0;
console.log('Imported expenses:', importedExpenses); const saved = [];
for (const e of importedExpenses) {
// Add the imported expenses to the store try {
dispatch(addExpenses(importedExpenses)); const res = await api.post('/expenses', e);
saved.push(res.data);
toast.success(`Successfully imported ${importedExpenses.length} expenses`); imported++;
} catch {}
}
dispatch(addExpenses(saved));
toast.success(`Successfully imported ${imported} expenses`);
} catch (error) { } catch (error) {
console.error('Import error:', error); console.error('Import error:', error);
@ -220,22 +224,15 @@ const Expenses = () => {
e.preventDefault(); e.preventDefault();
try { try {
if (editingExpense) { if (editingExpense) {
// Update existing expense const res = await api.put(`/expenses/${editingExpense}`, formData);
dispatch(updateExpense({ dispatch(updateExpense(res.data));
_id: editingExpense,
...formData
}));
toast.success('Expense updated successfully!'); toast.success('Expense updated successfully!');
setEditingExpense(null); setEditingExpense(null);
} else { } else {
// Add new expense const res = await api.post('/expenses', formData);
dispatch(addExpense({ dispatch(addExpense(res.data));
_id: `expense-${Date.now()}`,
...formData
}));
toast.success('Expense added successfully!'); toast.success('Expense added successfully!');
} }
setShowAddForm(false); setShowAddForm(false);
setFormData({ setFormData({
description: '', description: '',
@ -263,10 +260,15 @@ const Expenses = () => {
setShowAddForm(true); setShowAddForm(true);
}; };
const handleDelete = (expenseId: string) => { const handleDelete = async (expenseId: string) => {
if (window.confirm('Are you sure you want to delete this expense?')) { if (window.confirm('Are you sure you want to delete this expense?')) {
dispatch(deleteExpense(expenseId)); try {
toast.success('Expense deleted successfully!'); await api.delete(`/expenses/${expenseId}`);
dispatch(deleteExpense(expenseId));
toast.success('Expense deleted successfully!');
} catch {
toast.error('Failed to delete expense');
}
} }
}; };

View file

@ -1,34 +1,19 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import { RootState } from '../store'; 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 { addProduct } from '../store/slices/productSlice';
import { calculateOrderPrintingCost, calculateOrderProfit, enrichOrderItemsWithCosts } from '../utils/orderCalculations'; import { calculateOrderPrintingCost, calculateOrderProfit, enrichOrderItemsWithCosts } from '../utils/orderCalculations';
import { formatAustralianDate } from '../utils/dateFormatter'; import { formatAustralianDate } from '../utils/dateFormatter';
import { Search, Package, Download, ArrowRight, Edit, Trash2, X, Plus } from 'lucide-react'; import { Search, Package, Download, ArrowRight, Edit, Trash2, X, Plus } from 'lucide-react';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import api from '../utils/api';
const Orders = () => { const Orders = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { orders } = useSelector((state: RootState) => state.orders); const { orders } = useSelector((state: RootState) => state.orders);
const { products } = useSelector((state: RootState) => state.products); 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 [searchTerm, setSearchTerm] = useState('');
const [dateRange, setDateRange] = useState('all'); const [dateRange, setDateRange] = useState('all');
@ -161,11 +146,16 @@ const Orders = () => {
setShowEditModal(true); setShowEditModal(true);
}; };
const handleSaveOrder = (updatedOrder: Order) => { const handleSaveOrder = async (updatedOrder: Order) => {
dispatch(updateOrder(updatedOrder)); 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); setShowEditModal(false);
setEditingOrder(null); setEditingOrder(null);
toast.success('Order updated successfully');
}; };
const handleDeleteClick = (orderId: string) => { const handleDeleteClick = (orderId: string) => {
@ -173,17 +163,20 @@ const Orders = () => {
setShowDeleteConfirm(true); setShowDeleteConfirm(true);
}; };
const handleConfirmDelete = () => { const handleConfirmDelete = async () => {
if (deletingOrderId && deletingOrderId !== 'undefined') { if (deletingOrderId && deletingOrderId !== 'undefined') {
dispatch(deleteOrder(deletingOrderId)); try {
setShowDeleteConfirm(false); await api.delete(`/orders/${deletingOrderId}`);
setDeletingOrderId(null); dispatch(deleteOrder(deletingOrderId));
toast.success('Order deleted successfully'); toast.success('Order deleted successfully');
} catch {
toast.error('Failed to delete order');
}
} else { } else {
toast.error('Cannot delete order: Invalid order ID'); toast.error('Cannot delete order: Invalid order ID');
setShowDeleteConfirm(false);
setDeletingOrderId(null);
} }
setShowDeleteConfirm(false);
setDeletingOrderId(null);
}; };
const handleCancelDelete = () => { const handleCancelDelete = () => {
@ -202,29 +195,30 @@ const Orders = () => {
setShowNewProductModal(true); setShowNewProductModal(true);
}; };
const handleSaveNewProduct = () => { const handleSaveNewProduct = async () => {
if (!newProductData.title) { if (!newProductData.title) {
toast.error('Please enter a product title'); toast.error('Please enter a product title');
return; return;
} }
try {
const newProduct = { const res = await api.post('/products', {
_id: `product-${Date.now()}`, title: newProductData.title,
title: newProductData.title, description: '',
description: '', price: newProductData.price,
price: newProductData.price, costOfGoods: newProductData.costOfGoods,
costOfGoods: newProductData.costOfGoods, printingCost: newProductData.printingCost,
printingCost: newProductData.printingCost, sku: '',
sku: '', category: 'Other',
category: '', tags: [],
tags: [], inventory: { quantity: 0, lowStockAlert: 10 },
inventory: { quantity: 0, lowStockAlert: 10 }, isActive: true
isActive: true });
}; dispatch(addProduct(res.data));
setShowNewProductModal(false);
dispatch(addProduct(newProduct)); toast.success('Product created successfully');
setShowNewProductModal(false); } catch {
toast.success('Product created successfully'); toast.error('Failed to create product');
}
}; };
// Manual order handlers // Manual order handlers
@ -239,15 +233,13 @@ const Orders = () => {
setShowAddModal(true); setShowAddModal(true);
}; };
const handleSaveNewOrder = () => { const handleSaveNewOrder = async () => {
if (!newOrder.orderNumber || !newOrder.items[0]?.title) { if (!newOrder.orderNumber || !newOrder.items[0]?.title) {
toast.error('Please fill in order number and at least one item'); toast.error('Please fill in order number and at least one item');
return; return;
} }
const calculatedTotal = newOrder.items.reduce((sum, item) => sum + (item.quantity * item.price), 0); 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 const enrichedItems = newOrder.items
.filter(item => item.title.trim() !== '') .filter(item => item.title.trim() !== '')
.map(item => { .map(item => {
@ -259,19 +251,22 @@ const Orders = () => {
}; };
}); });
const orderToSave: Order = { try {
_id: `fb-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, // More unique ID const res = await api.post('/orders', {
orderNumber: `FB-${newOrder.orderNumber}`, // Prefix to identify Facebook orders orderNumber: `FB-${newOrder.orderNumber}`,
customer: newOrder.customer, customer: newOrder.customer,
dateOrdered: newOrder.dateOrdered, dateOrdered: newOrder.dateOrdered,
total: calculatedTotal, total: calculatedTotal,
status: 'delivered' as const, status: 'delivered',
items: enrichedItems paymentStatus: 'paid',
}; items: enrichedItems
});
dispatch(addOrder(orderToSave)); dispatch(addOrder(res.data));
setShowAddModal(false); setShowAddModal(false);
toast.success('Facebook Marketplace order added successfully'); toast.success('Facebook Marketplace order added successfully');
} catch {
toast.error('Failed to save order');
}
}; };
const handleAddItem = () => { const handleAddItem = () => {

View file

@ -1,9 +1,10 @@
import React, { useState, useRef } from 'react'; import React, { useState, useRef } from 'react';
import { useSelector, useDispatch } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import { RootState } from '../store'; 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 { Upload, Plus, Search, Edit, Trash2, Package } from 'lucide-react';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import api from '../utils/api';
interface ProductFormData { interface ProductFormData {
title: string; title: string;
@ -68,12 +69,12 @@ const Products = () => {
const importedProducts = []; const importedProducts = [];
for (let i = 1; i < lines.length; i++) { for (let i = 1; i < lines.length; i++) {
if (lines[i].trim() === '') continue; if (lines[i].trim() === '') continue;
const values = lines[i].split(',').map(v => v.trim().replace(/"/g, '')); const values = lines[i].split(',').map(v => v.trim().replace(/"/g, ''));
const productData: any = { const productData: any = {
printingCost: 0 // Default printing cost printingCost: 0
}; };
headers.forEach((header, index) => { headers.forEach((header, index) => {
const value = values[index] || ''; const value = values[index] || '';
switch (header.toLowerCase()) { switch (header.toLowerCase()) {
@ -119,19 +120,22 @@ const Products = () => {
if (productData.title) { if (productData.title) {
importedProducts.push({ importedProducts.push({
...productData, ...productData,
inventory: { inventory: { quantity: productData.quantity || 0, lowStockAlert: 5 },
quantity: productData.quantity || 0,
lowStockAlert: 5
},
tags: productData.tags || [], tags: productData.tags || [],
isActive: true isActive: true
}); });
} }
} }
// Simulate API call - replace with actual API call let imported = 0;
console.log('Imported products:', importedProducts); for (const p of importedProducts) {
toast.success(`Successfully imported ${importedProducts.length} products`); try {
const res = await api.post('/products', p);
dispatch(addProduct(res.data));
imported++;
} catch {}
}
toast.success(`Successfully imported ${imported} products`);
} catch (error) { } catch (error) {
console.error('Import error:', error); console.error('Import error:', error);
@ -157,15 +161,13 @@ const Products = () => {
const handleAddProduct = async () => { const handleAddProduct = async () => {
try { try {
// Simulate API call - replace with actual API call const payload = {
const newProduct = {
...formData, ...formData,
_id: `temp-${Date.now()}`,
tags: formData.tags.split(',').map(t => t.trim()).filter(t => t), tags: formData.tags.split(',').map(t => t.trim()).filter(t => t),
isActive: true isActive: true
}; };
const res = await api.post('/products', payload);
console.log('New product:', newProduct); dispatch(addProduct(res.data));
toast.success('Product added successfully'); toast.success('Product added successfully');
setShowAddForm(false); setShowAddForm(false);
setFormData({ setFormData({
@ -208,16 +210,15 @@ const Products = () => {
const handleUpdateProduct = async () => { const handleUpdateProduct = async () => {
if (!editingProduct) return; if (!editingProduct) return;
try { try {
const updatedProduct = { const payload = {
_id: editingProduct,
...formData, ...formData,
tags: formData.tags.split(',').map(t => t.trim()).filter(t => t), tags: formData.tags.split(',').map(t => t.trim()).filter(t => t),
isActive: true isActive: true
}; };
const res = await api.put(`/products/${editingProduct}`, payload);
dispatch(updateProduct(updatedProduct)); dispatch(updateProduct(res.data));
toast.success('Product updated successfully'); toast.success('Product updated successfully');
setShowAddForm(false); setShowAddForm(false);
setEditingProduct(null); setEditingProduct(null);
@ -241,10 +242,15 @@ const Products = () => {
setShowDeleteConfirm(productId); setShowDeleteConfirm(productId);
}; };
const confirmDeleteProduct = () => { const confirmDeleteProduct = async () => {
if (showDeleteConfirm) { if (showDeleteConfirm) {
dispatch(deleteProduct(showDeleteConfirm)); try {
toast.success('Product deleted successfully'); await api.delete(`/products/${showDeleteConfirm}`);
dispatch(deleteProduct(showDeleteConfirm));
toast.success('Product deleted successfully');
} catch {
toast.error('Failed to delete product');
}
setShowDeleteConfirm(null); setShowDeleteConfirm(null);
} }
}; };

View file

@ -15,7 +15,7 @@ interface CustomerState {
} }
const initialState: CustomerState = { const initialState: CustomerState = {
customers: JSON.parse(localStorage.getItem('customers') || '[]'), customers: [],
loading: false, loading: false,
error: null, error: null,
}; };
@ -26,18 +26,13 @@ const customerSlice = createSlice({
reducers: { reducers: {
setCustomers: (state, action: PayloadAction<Customer[]>) => { setCustomers: (state, action: PayloadAction<Customer[]>) => {
state.customers = action.payload; state.customers = action.payload;
localStorage.setItem('customers', JSON.stringify(state.customers));
}, },
addCustomer: (state, action: PayloadAction<Customer>) => { addCustomer: (state, action: PayloadAction<Customer>) => {
state.customers.push(action.payload); state.customers.push(action.payload);
localStorage.setItem('customers', JSON.stringify(state.customers));
}, },
updateCustomer: (state, action: PayloadAction<Customer>) => { updateCustomer: (state, action: PayloadAction<Customer>) => {
const index = state.customers.findIndex(c => c._id === action.payload._id); const index = state.customers.findIndex(c => c._id === action.payload._id);
if (index !== -1) { if (index !== -1) state.customers[index] = action.payload;
state.customers[index] = action.payload;
localStorage.setItem('customers', JSON.stringify(state.customers));
}
}, },
setLoading: (state, action: PayloadAction<boolean>) => { setLoading: (state, action: PayloadAction<boolean>) => {
state.loading = action.payload; state.loading = action.payload;
@ -49,4 +44,4 @@ const customerSlice = createSlice({
}); });
export const { setCustomers, addCustomer, updateCustomer, setLoading, setError } = customerSlice.actions; export const { setCustomers, addCustomer, updateCustomer, setLoading, setError } = customerSlice.actions;
export default customerSlice.reducer; export default customerSlice.reducer;

View file

@ -18,7 +18,7 @@ interface ExpenseState {
} }
const initialState: ExpenseState = { const initialState: ExpenseState = {
expenses: JSON.parse(localStorage.getItem('expenses') || '[]'), expenses: [],
loading: false, loading: false,
error: null, error: null,
}; };
@ -29,26 +29,19 @@ const expenseSlice = createSlice({
reducers: { reducers: {
setExpenses: (state, action: PayloadAction<Expense[]>) => { setExpenses: (state, action: PayloadAction<Expense[]>) => {
state.expenses = action.payload; state.expenses = action.payload;
localStorage.setItem('expenses', JSON.stringify(state.expenses));
}, },
addExpenses: (state, action: PayloadAction<Expense[]>) => { addExpenses: (state, action: PayloadAction<Expense[]>) => {
state.expenses.push(...action.payload); state.expenses.push(...action.payload);
localStorage.setItem('expenses', JSON.stringify(state.expenses));
}, },
addExpense: (state, action: PayloadAction<Expense>) => { addExpense: (state, action: PayloadAction<Expense>) => {
state.expenses.push(action.payload); state.expenses.push(action.payload);
localStorage.setItem('expenses', JSON.stringify(state.expenses));
}, },
updateExpense: (state, action: PayloadAction<Expense>) => { updateExpense: (state, action: PayloadAction<Expense>) => {
const index = state.expenses.findIndex(e => e._id === action.payload._id); const index = state.expenses.findIndex(e => e._id === action.payload._id);
if (index !== -1) { if (index !== -1) state.expenses[index] = action.payload;
state.expenses[index] = action.payload;
localStorage.setItem('expenses', JSON.stringify(state.expenses));
}
}, },
deleteExpense: (state, action: PayloadAction<string>) => { deleteExpense: (state, action: PayloadAction<string>) => {
state.expenses = state.expenses.filter(e => e._id !== action.payload); state.expenses = state.expenses.filter(e => e._id !== action.payload);
localStorage.setItem('expenses', JSON.stringify(state.expenses));
}, },
setLoading: (state, action: PayloadAction<boolean>) => { setLoading: (state, action: PayloadAction<boolean>) => {
state.loading = action.payload; state.loading = action.payload;
@ -60,4 +53,4 @@ const expenseSlice = createSlice({
}); });
export const { setExpenses, addExpenses, addExpense, updateExpense, deleteExpense, setLoading, setError } = expenseSlice.actions; export const { setExpenses, addExpenses, addExpense, updateExpense, deleteExpense, setLoading, setError } = expenseSlice.actions;
export default expenseSlice.reducer; export default expenseSlice.reducer;

View file

@ -38,7 +38,7 @@ interface OrderState {
} }
const initialState: OrderState = { const initialState: OrderState = {
orders: JSON.parse(localStorage.getItem('orders') || '[]'), orders: [],
loading: false, loading: false,
error: null, error: null,
}; };
@ -49,26 +49,19 @@ const orderSlice = createSlice({
reducers: { reducers: {
setOrders: (state, action: PayloadAction<Order[]>) => { setOrders: (state, action: PayloadAction<Order[]>) => {
state.orders = action.payload; state.orders = action.payload;
localStorage.setItem('orders', JSON.stringify(state.orders));
}, },
addOrder: (state, action: PayloadAction<Order>) => { addOrder: (state, action: PayloadAction<Order>) => {
state.orders.push(action.payload); state.orders.push(action.payload);
localStorage.setItem('orders', JSON.stringify(state.orders));
}, },
addOrders: (state, action: PayloadAction<Order[]>) => { addOrders: (state, action: PayloadAction<Order[]>) => {
state.orders.push(...action.payload); state.orders.push(...action.payload);
localStorage.setItem('orders', JSON.stringify(state.orders));
}, },
updateOrder: (state, action: PayloadAction<Order>) => { updateOrder: (state, action: PayloadAction<Order>) => {
const index = state.orders.findIndex(o => o._id === action.payload._id); const index = state.orders.findIndex(o => o._id === action.payload._id);
if (index !== -1) { if (index !== -1) state.orders[index] = action.payload;
state.orders[index] = action.payload;
localStorage.setItem('orders', JSON.stringify(state.orders));
}
}, },
deleteOrder: (state, action: PayloadAction<string>) => { deleteOrder: (state, action: PayloadAction<string>) => {
state.orders = state.orders.filter(o => o._id !== action.payload); state.orders = state.orders.filter(o => o._id !== action.payload);
localStorage.setItem('orders', JSON.stringify(state.orders));
}, },
setLoading: (state, action: PayloadAction<boolean>) => { setLoading: (state, action: PayloadAction<boolean>) => {
state.loading = action.payload; state.loading = action.payload;
@ -80,4 +73,4 @@ const orderSlice = createSlice({
}); });
export const { setOrders, addOrder, addOrders, updateOrder, deleteOrder, setLoading, setError } = orderSlice.actions; export const { setOrders, addOrder, addOrders, updateOrder, deleteOrder, setLoading, setError } = orderSlice.actions;
export default orderSlice.reducer; export default orderSlice.reducer;

View file

@ -24,7 +24,7 @@ interface ProductState {
} }
const initialState: ProductState = { const initialState: ProductState = {
products: JSON.parse(localStorage.getItem('products') || '[]'), products: [],
loading: false, loading: false,
error: null, error: null,
}; };
@ -35,22 +35,16 @@ const productSlice = createSlice({
reducers: { reducers: {
setProducts: (state, action: PayloadAction<Product[]>) => { setProducts: (state, action: PayloadAction<Product[]>) => {
state.products = action.payload; state.products = action.payload;
localStorage.setItem('products', JSON.stringify(state.products));
}, },
addProduct: (state, action: PayloadAction<Product>) => { addProduct: (state, action: PayloadAction<Product>) => {
state.products.push(action.payload); state.products.push(action.payload);
localStorage.setItem('products', JSON.stringify(state.products));
}, },
updateProduct: (state, action: PayloadAction<Product>) => { updateProduct: (state, action: PayloadAction<Product>) => {
const index = state.products.findIndex(p => p._id === action.payload._id); const index = state.products.findIndex(p => p._id === action.payload._id);
if (index !== -1) { if (index !== -1) state.products[index] = action.payload;
state.products[index] = action.payload;
localStorage.setItem('products', JSON.stringify(state.products));
}
}, },
deleteProduct: (state, action: PayloadAction<string>) => { deleteProduct: (state, action: PayloadAction<string>) => {
state.products = state.products.filter(p => p._id !== action.payload); state.products = state.products.filter(p => p._id !== action.payload);
localStorage.setItem('products', JSON.stringify(state.products));
}, },
setLoading: (state, action: PayloadAction<boolean>) => { setLoading: (state, action: PayloadAction<boolean>) => {
state.loading = action.payload; state.loading = action.payload;
@ -62,4 +56,4 @@ const productSlice = createSlice({
}); });
export const { setProducts, addProduct, updateProduct, deleteProduct, setLoading, setError } = productSlice.actions; export const { setProducts, addProduct, updateProduct, deleteProduct, setLoading, setError } = productSlice.actions;
export default productSlice.reducer; export default productSlice.reducer;

View file

@ -31,8 +31,8 @@ app.use(cors({
// Rate limiting // Rate limiting
const limiter = rateLimit({ const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes windowMs: 15 * 60 * 1000,
max: 100, // limit each IP to 100 requests per windowMs max: 1000,
message: 'Too many requests from this IP, please try again later.', message: 'Too many requests from this IP, please try again later.',
}); });
app.use(limiter); app.use(limiter);

View file

@ -1,10 +1,13 @@
import mongoose, { Document, Schema } from 'mongoose'; import mongoose, { Document, Schema } from 'mongoose';
export interface IExpense extends Document { export interface IExpense extends Document {
category: 'materials' | 'shipping' | 'fees' | 'advertising' | 'tools' | 'other'; category: string;
description: string; description: string;
amount: number; amount: number;
date: Date; date: Date;
taxDeductible: boolean;
vendor?: string;
reference?: string;
receiptUrl?: string; receiptUrl?: string;
notes?: string; notes?: string;
userId: mongoose.Types.ObjectId; userId: mongoose.Types.ObjectId;
@ -13,14 +16,13 @@ export interface IExpense extends Document {
} }
const ExpenseSchema: Schema = new Schema({ const ExpenseSchema: Schema = new Schema({
category: { category: { type: String, required: true },
type: String,
enum: ['materials', 'shipping', 'fees', 'advertising', 'tools', 'other'],
required: true,
},
description: { type: String, required: true, trim: true }, description: { type: String, required: true, trim: true },
amount: { type: Number, required: true, min: 0 }, amount: { type: Number, required: true, min: 0 },
date: { type: Date, required: true }, date: { type: Date, required: true },
taxDeductible: { type: Boolean, default: false },
vendor: { type: String },
reference: { type: String },
receiptUrl: { type: String }, receiptUrl: { type: String },
notes: { type: String }, notes: { type: String },
userId: { type: Schema.Types.ObjectId, ref: 'User', required: true, index: true }, userId: { type: Schema.Types.ObjectId, ref: 'User', required: true, index: true },

View file

@ -3,29 +3,33 @@ import mongoose, { Document, Schema } from 'mongoose';
export interface IOrder extends Document { export interface IOrder extends Document {
orderNumber: string; orderNumber: string;
etsyOrderId?: string; etsyOrderId?: string;
customerId: mongoose.Types.ObjectId; customerId?: mongoose.Types.ObjectId;
customer?: { name: string; email: string };
items: { items: {
productId: mongoose.Types.ObjectId; productId?: mongoose.Types.ObjectId;
sku: string; sku?: string;
title: string; title: string;
quantity: number; quantity: number;
price: number; price: number;
printingCost?: number;
costOfGoods?: number;
variant?: string; variant?: string;
}[]; }[];
fees?: { etsy: number; processing: number; shipping: number };
subtotal: number; subtotal: number;
shipping: number; shipping: number;
tax: number; tax: number;
total: number; total: number;
status: 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled' | 'refunded'; status: 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled' | 'refunded';
paymentStatus: 'pending' | 'paid' | 'refunded' | 'failed'; paymentStatus: 'pending' | 'paid' | 'refunded' | 'failed';
shippingAddress: { shippingAddress?: {
name: string; name?: string;
street1: string; street1?: string;
street2?: string; street2?: string;
city: string; city?: string;
state: string; state?: string;
zipCode: string; zipCode?: string;
country: string; country?: string;
}; };
trackingNumber?: string; trackingNumber?: string;
shippingCarrier?: string; shippingCarrier?: string;
@ -39,39 +43,50 @@ export interface IOrder extends Document {
} }
const OrderSchema: Schema = new Schema({ const OrderSchema: Schema = new Schema({
orderNumber: { type: String, required: true, unique: true }, orderNumber: { type: String, required: true },
etsyOrderId: { type: String }, 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: [{ items: [{
productId: { type: Schema.Types.ObjectId, ref: 'Product', required: true }, productId: { type: Schema.Types.ObjectId, ref: 'Product' },
sku: { type: String, required: true }, sku: { type: String },
title: { type: String, required: true }, title: { type: String, required: true },
quantity: { type: Number, required: true, min: 1 }, quantity: { type: Number, required: true, min: 1 },
price: { type: Number, required: true, min: 0 }, 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 }, fees: {
shipping: { type: Number, required: true, min: 0 }, etsy: { type: Number, default: 0 },
tax: { type: Number, required: true, min: 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 }, total: { type: Number, required: true, min: 0 },
status: { status: {
type: String, type: String,
enum: ['pending', 'processing', 'shipped', 'delivered', 'cancelled', 'refunded'], enum: ['pending', 'processing', 'shipped', 'delivered', 'cancelled', 'refunded'],
default: 'pending' default: 'pending',
}, },
paymentStatus: { paymentStatus: {
type: String, type: String,
enum: ['pending', 'paid', 'refunded', 'failed'], enum: ['pending', 'paid', 'refunded', 'failed'],
default: 'pending' default: 'paid',
}, },
shippingAddress: { shippingAddress: {
name: { type: String, required: true }, name: { type: String },
street1: { type: String, required: true }, street1: { type: String },
street2: { type: String }, street2: { type: String },
city: { type: String, required: true }, city: { type: String },
state: { type: String, required: true }, state: { type: String },
zipCode: { type: String, required: true }, zipCode: { type: String },
country: { type: String, required: true } country: { type: String },
}, },
trackingNumber: { type: String }, trackingNumber: { type: String },
shippingCarrier: { type: String }, shippingCarrier: { type: String },
@ -81,12 +96,14 @@ const OrderSchema: Schema = new Schema({
dateDelivered: { type: Date }, dateDelivered: { type: Date },
userId: { type: Schema.Types.ObjectId, ref: 'User', required: true, index: true }, userId: { type: Schema.Types.ObjectId, ref: 'User', required: true, index: true },
dateCreated: { type: Date, default: Date.now }, 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(); this.dateUpdated = new Date();
next(); next();
}); });
export default mongoose.model<IOrder>('Order', OrderSchema); export default mongoose.model<IOrder>('Order', OrderSchema);

View file

@ -5,6 +5,7 @@ export interface IProduct extends Document {
description: string; description: string;
price: number; price: number;
costOfGoods: number; costOfGoods: number;
printingCost: number;
sku: string; sku: string;
category: string; category: string;
tags: string[]; tags: string[];
@ -36,11 +37,12 @@ export interface IProduct extends Document {
const ProductSchema: Schema = new Schema({ const ProductSchema: Schema = new Schema({
title: { type: String, required: true, trim: true }, title: { type: String, required: true, trim: true },
description: { type: String, required: true }, description: { type: String, default: '' },
price: { type: Number, required: true, min: 0 }, price: { type: Number, required: true, min: 0 },
costOfGoods: { type: Number, required: true, min: 0 }, costOfGoods: { type: Number, default: 0, min: 0 },
sku: { type: String, required: true, unique: true, trim: true }, printingCost: { type: Number, default: 0, min: 0 },
category: { type: String, required: true }, sku: { type: String, trim: true, default: '' },
category: { type: String, default: 'Other' },
tags: [{ type: String, trim: true }], tags: [{ type: String, trim: true }],
images: [{ type: String }], images: [{ type: String }],
variants: [{ variants: [{

View file

@ -7,13 +7,12 @@ const router = Router();
router.get('/', async (req: AuthRequest, res: Response) => { router.get('/', async (req: AuthRequest, res: Response) => {
try { 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 }; const filter: any = { userId: req.userId };
if (status) filter.status = status; if (status) filter.status = status;
if (paymentStatus) filter.paymentStatus = paymentStatus; if (paymentStatus) filter.paymentStatus = paymentStatus;
const orders = await Order.find(filter) const orders = await Order.find(filter)
.populate('customerId', 'name email')
.sort({ dateOrdered: -1 }) .sort({ dateOrdered: -1 })
.limit(Number(limit)) .limit(Number(limit))
.skip((Number(page) - 1) * 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) => { router.post('/', async (req: AuthRequest, res: Response) => {
try { 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 order.save();
await Customer.findOneAndUpdate( if (order.customerId) {
{ _id: order.customerId, userId: req.userId }, await Customer.findOneAndUpdate(
{ $inc: { totalOrders: 1, totalSpent: order.total } } { _id: order.customerId, userId: req.userId },
); { $inc: { totalOrders: 1, totalSpent: order.total } }
);
}
res.status(201).json(order); res.status(201).json(order);
} catch (err) { } 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) => { router.get('/:id', async (req: AuthRequest, res: Response) => {
try { try {
const order = await Order.findOne({ _id: req.params.id, userId: req.userId }) const order = await Order.findOne({ _id: req.params.id, userId: req.userId });
.populate('customerId', 'name email');
if (!order) return res.status(404).json({ message: 'Order not found' }); if (!order) return res.status(404).json({ message: 'Order not found' });
res.json(order); res.json(order);
} catch (err) { } catch (err) {
@ -54,9 +82,10 @@ router.get('/:id', async (req: AuthRequest, res: Response) => {
router.put('/:id', async (req: AuthRequest, res: Response) => { router.put('/:id', async (req: AuthRequest, res: Response) => {
try { try {
const { _id, ...body } = req.body;
const order = await Order.findOneAndUpdate( const order = await Order.findOneAndUpdate(
{ _id: req.params.id, userId: req.userId }, { _id: req.params.id, userId: req.userId },
req.body, body,
{ new: true, runValidators: true } { new: true, runValidators: true }
); );
if (!order) return res.status(404).json({ message: 'Order not found' }); 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; export default router;