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:
parent
0d42d97d70
commit
1a3bd33be8
15 changed files with 408 additions and 314 deletions
|
|
@ -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 (
|
||||
<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 (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<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>
|
||||
</div>
|
||||
<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">
|
||||
Analytics
|
||||
</a>
|
||||
<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">
|
||||
Profit Analysis
|
||||
</a>
|
||||
<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>
|
||||
{navLink('/analytics', 'Analytics')}
|
||||
{navLink('/profit-analysis', 'Profit Analysis')}
|
||||
{navLink('/products', 'Products')}
|
||||
{navLink('/orders', 'Orders')}
|
||||
{navLink('/expenses', 'Expenses')}
|
||||
{navLink('/data-import', 'Data Import')}
|
||||
</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>
|
||||
</nav>
|
||||
|
|
|
|||
|
|
@ -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,7 +40,7 @@ export const MissingProductsModal: React.FC<MissingProductsModalProps> = ({
|
|||
}));
|
||||
};
|
||||
|
||||
const handleAddProducts = () => {
|
||||
const handleAddProducts = async () => {
|
||||
const newProducts: any[] = [];
|
||||
|
||||
for (const product of missingProducts) {
|
||||
|
|
@ -47,31 +48,42 @@ export const MissingProductsModal: React.FC<MissingProductsModalProps> = ({
|
|||
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);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -71,7 +72,7 @@ const Products = () => {
|
|||
|
||||
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) => {
|
||||
|
|
@ -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({
|
||||
|
|
@ -210,14 +212,13 @@ const Products = () => {
|
|||
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);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<Customer[]>) => {
|
||||
state.customers = action.payload;
|
||||
localStorage.setItem('customers', JSON.stringify(state.customers));
|
||||
},
|
||||
addCustomer: (state, action: PayloadAction<Customer>) => {
|
||||
state.customers.push(action.payload);
|
||||
localStorage.setItem('customers', JSON.stringify(state.customers));
|
||||
},
|
||||
updateCustomer: (state, action: PayloadAction<Customer>) => {
|
||||
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<boolean>) => {
|
||||
state.loading = action.payload;
|
||||
|
|
|
|||
|
|
@ -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<Expense[]>) => {
|
||||
state.expenses = action.payload;
|
||||
localStorage.setItem('expenses', JSON.stringify(state.expenses));
|
||||
},
|
||||
addExpenses: (state, action: PayloadAction<Expense[]>) => {
|
||||
state.expenses.push(...action.payload);
|
||||
localStorage.setItem('expenses', JSON.stringify(state.expenses));
|
||||
},
|
||||
addExpense: (state, action: PayloadAction<Expense>) => {
|
||||
state.expenses.push(action.payload);
|
||||
localStorage.setItem('expenses', JSON.stringify(state.expenses));
|
||||
},
|
||||
updateExpense: (state, action: PayloadAction<Expense>) => {
|
||||
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<string>) => {
|
||||
state.expenses = state.expenses.filter(e => e._id !== action.payload);
|
||||
localStorage.setItem('expenses', JSON.stringify(state.expenses));
|
||||
},
|
||||
setLoading: (state, action: PayloadAction<boolean>) => {
|
||||
state.loading = action.payload;
|
||||
|
|
|
|||
|
|
@ -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<Order[]>) => {
|
||||
state.orders = action.payload;
|
||||
localStorage.setItem('orders', JSON.stringify(state.orders));
|
||||
},
|
||||
addOrder: (state, action: PayloadAction<Order>) => {
|
||||
state.orders.push(action.payload);
|
||||
localStorage.setItem('orders', JSON.stringify(state.orders));
|
||||
},
|
||||
addOrders: (state, action: PayloadAction<Order[]>) => {
|
||||
state.orders.push(...action.payload);
|
||||
localStorage.setItem('orders', JSON.stringify(state.orders));
|
||||
},
|
||||
updateOrder: (state, action: PayloadAction<Order>) => {
|
||||
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<string>) => {
|
||||
state.orders = state.orders.filter(o => o._id !== action.payload);
|
||||
localStorage.setItem('orders', JSON.stringify(state.orders));
|
||||
},
|
||||
setLoading: (state, action: PayloadAction<boolean>) => {
|
||||
state.loading = action.payload;
|
||||
|
|
|
|||
|
|
@ -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<Product[]>) => {
|
||||
state.products = action.payload;
|
||||
localStorage.setItem('products', JSON.stringify(state.products));
|
||||
},
|
||||
addProduct: (state, action: PayloadAction<Product>) => {
|
||||
state.products.push(action.payload);
|
||||
localStorage.setItem('products', JSON.stringify(state.products));
|
||||
},
|
||||
updateProduct: (state, action: PayloadAction<Product>) => {
|
||||
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<string>) => {
|
||||
state.products = state.products.filter(p => p._id !== action.payload);
|
||||
localStorage.setItem('products', JSON.stringify(state.products));
|
||||
},
|
||||
setLoading: (state, action: PayloadAction<boolean>) => {
|
||||
state.loading = action.payload;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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,10 +96,12 @@ 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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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: [{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue