etsy-finance-tracker/client/src/pages/Orders.tsx
dlawler489 1a3bd33be8 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>
2026-04-22 08:48:05 +10:00

950 lines
No EOL
40 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { RootState } from '../store';
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);
const [searchTerm, setSearchTerm] = useState('');
const [dateRange, setDateRange] = useState('all');
// Edit modal state
const [editingOrder, setEditingOrder] = useState<Order | null>(null);
const [showEditModal, setShowEditModal] = useState(false);
// Delete confirmation state
const [deletingOrderId, setDeletingOrderId] = useState<string | null>(null);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
// Add manual order state
const [showAddModal, setShowAddModal] = useState(false);
const [showNewProductModal, setShowNewProductModal] = useState(false);
const [newProductData, setNewProductData] = useState({
title: '',
price: 0,
printingCost: 0,
costOfGoods: 0
});
const [newOrder, setNewOrder] = useState({
orderNumber: '',
customer: { name: '', email: '' },
dateOrdered: new Date().toISOString().split('T')[0],
total: 0,
items: [{ title: '', quantity: 1, price: 0 }]
});
// Helper functions to use current product costs
const getUpdatedPrintingCost = (order: Order) => {
const updatedItems = enrichOrderItemsWithCosts(order.items || [], products || []);
return calculateOrderPrintingCost(updatedItems);
};
const getUpdatedProfit = (order: Order) => {
const updatedItems = enrichOrderItemsWithCosts(order.items || [], products || []);
return calculateOrderProfit(updatedItems, order.total || 0);
};
// Sort orders by newest first, then apply filters
const sortedOrders = [...orders].sort((a, b) =>
new Date(b.dateOrdered).getTime() - new Date(a.dateOrdered).getTime()
);
const filteredOrders = sortedOrders.filter(order => {
const matchesSearch = order.orderNumber.toLowerCase().includes(searchTerm.toLowerCase()) ||
order.customer?.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
order.customer?.email?.toLowerCase().includes(searchTerm.toLowerCase());
let matchesDate = true;
if (dateRange !== 'all') {
const orderDate = new Date(order.dateOrdered);
const now = new Date();
// Debug logging
console.log('Filtering order:', {
orderNumber: order.orderNumber,
dateOrdered: order.dateOrdered,
parsedDate: orderDate,
now: now,
dateRange: dateRange
});
const timeDiff = now.getTime() - orderDate.getTime();
const daysDiff = timeDiff / (24 * 60 * 60 * 1000);
switch (dateRange) {
case 'week':
matchesDate = daysDiff >= 0 && daysDiff <= 7;
break;
case 'month':
matchesDate = daysDiff >= 0 && daysDiff <= 30;
break;
case 'quarter':
matchesDate = daysDiff >= 0 && daysDiff <= 90;
break;
case 'year':
matchesDate = daysDiff >= 0 && daysDiff <= 365;
break;
}
console.log('Date filter result:', { daysDiff, matchesDate });
}
return matchesSearch && matchesDate;
});
// Financial calculations based on filtered results
const totalPrintingCosts = filteredOrders.reduce((sum, order) => sum + getUpdatedPrintingCost(order), 0);
const handleExportCSV = () => {
const headers = [
'Order Number', 'Customer Name', 'Date', 'Revenue', 'Printing Cost', 'Profit', 'Items'
];
const csvData = filteredOrders.map(order => {
const printingCost = getUpdatedPrintingCost(order);
const profit = getUpdatedProfit(order);
return [
order.orderNumber,
order.customer?.name || '',
formatAustralianDate(order.dateOrdered),
`$${order.total.toFixed(2)}`,
`$${printingCost.toFixed(2)}`,
`$${profit.toFixed(2)}`,
order.items?.map(item => `${item.title} (${item.quantity})`).join('; ') || ''
];
});
const csv = [headers, ...csvData].map((row: string[]) =>
row.map((field: string) => `"${field}"`).join(',')
).join('\n');
const blob = new Blob([csv], { type: 'text/csv' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `etsy-orders-${new Date().toISOString().split('T')[0]}.csv`;
a.click();
window.URL.revokeObjectURL(url);
toast.success('Financial data exported successfully');
};
// Handler functions for CRUD operations
const handleEditOrder = (order: Order) => {
setEditingOrder({ ...order });
setShowEditModal(true);
};
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);
};
const handleDeleteClick = (orderId: string) => {
setDeletingOrderId(orderId);
setShowDeleteConfirm(true);
};
const handleConfirmDelete = async () => {
if (deletingOrderId && deletingOrderId !== 'undefined') {
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);
};
const handleCancelDelete = () => {
setShowDeleteConfirm(false);
setDeletingOrderId(null);
};
// New product creation handlers
const handleCreateNewProduct = (productTitle: string) => {
setNewProductData({
title: productTitle,
price: 0,
printingCost: 0,
costOfGoods: 0
});
setShowNewProductModal(true);
};
const handleSaveNewProduct = async () => {
if (!newProductData.title) {
toast.error('Please enter a product title');
return;
}
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
const handleAddOrder = () => {
setNewOrder({
orderNumber: '',
customer: { name: '', email: '' },
dateOrdered: new Date().toISOString().split('T')[0],
total: 0,
items: [{ title: '', quantity: 1, price: 0 }]
});
setShowAddModal(true);
};
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);
const enrichedItems = newOrder.items
.filter(item => item.title.trim() !== '')
.map(item => {
const matchingProduct = products?.find(p => p.title.toLowerCase() === item.title.toLowerCase());
return {
...item,
printingCost: matchingProduct?.printingCost || 0,
costOfGoods: matchingProduct?.costOfGoods || 0
};
});
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 = () => {
setNewOrder(prev => ({
...prev,
items: [...prev.items, { title: '', quantity: 1, price: 0 }]
}));
};
const handleRemoveItem = (index: number) => {
if (newOrder.items.length > 1) {
setNewOrder(prev => ({
...prev,
items: prev.items.filter((_, i) => i !== index)
}));
}
};
const updateNewOrderItem = (index: number, field: string, value: any) => {
setNewOrder(prev => ({
...prev,
items: prev.items.map((item, i) =>
i === index ? { ...item, [field]: value } : item
)
}));
};
return (
<div className="p-6 max-w-7xl mx-auto">
<div className="flex justify-between items-center mb-6">
<div>
<h1 className="text-3xl font-bold text-gray-900">Financial Overview</h1>
<p className="text-gray-600 mt-1">Track revenue, costs, and profit from your Etsy orders</p>
</div>
<div className="flex gap-3">
<button
onClick={handleAddOrder}
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700"
>
<Plus className="w-4 h-4" />
Add Manual Order
</button>
<button
onClick={handleExportCSV}
className="flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700"
>
<Download className="w-4 h-4" />
Export Financial Data
</button>
<a
href="/data-import"
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
<ArrowRight className="w-4 h-4" />
Import Data
</a>
</div>
</div>
{/* Financial Summary Cards */}
<div className="grid grid-cols-1 md:grid-cols-1 max-w-md gap-6 mb-6">
<div className="bg-white p-6 rounded-lg shadow">
<div className="flex items-center">
<div className="p-3 rounded-full bg-orange-100">
<Package className="w-6 h-6 text-orange-600" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-600">Total Printing Costs</p>
<p className="text-2xl font-bold text-gray-900">${totalPrintingCosts.toFixed(2)}</p>
</div>
</div>
</div>
</div>
{/* Import Instructions */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
<h3 className="text-blue-800 font-semibold mb-2">💡 Need to import order data?</h3>
<p className="text-blue-700 text-sm">
Use the <a href="/data-import" className="font-semibold underline">Data Import</a> page to upload:
<span className="ml-2"> Etsy CSV statements</span>
<span className="ml-2"> Australia Post shipping data</span>
<span className="ml-2"> PDF packing slips</span>
</p>
</div>
{/* Search and Filters */}
<div className="bg-white p-4 rounded-lg shadow mb-6">
<div className="flex flex-wrap gap-4 items-center">
<div className="relative flex-1 min-w-64">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<input
type="text"
placeholder="Search by order number or customer..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<select
value={dateRange}
onChange={(e) => setDateRange(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="all">All Time</option>
<option value="week">Last Week</option>
<option value="month">Last Month</option>
<option value="quarter">Last Quarter</option>
<option value="year">Last Year</option>
</select>
</div>
</div>
{/* Financial Data Table */}
<div className="grid gap-6">
{filteredOrders.length === 0 ? (
<div className="text-center py-12">
<Package className="w-16 h-16 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">No orders found</h3>
<p className="text-gray-500 mb-4">Import your sales data using the Data Import page.</p>
<div className="flex gap-3 justify-center">
<a
href="/data-import"
className="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
<ArrowRight className="w-4 h-4" />
Go to Data Import
</a>
</div>
</div>
) : (
<div className="bg-white rounded-lg shadow overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<div className="flex items-center gap-1">
Order Details
<span className="text-xs text-gray-400">( newest first)</span>
</div>
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Customer
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Items
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Revenue
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Printing Cost
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Profit
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Margin %
</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{filteredOrders.map((order) => {
const printingCost = getUpdatedPrintingCost(order);
const profit = getUpdatedProfit(order);
const margin = order.total > 0 ? ((profit / order.total) * 100) : 0;
// Use order number as fallback key if _id is undefined
const orderKey = order._id || `fallback-${order.orderNumber}`;
return (
<tr key={orderKey} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div>
<div className="text-sm font-medium text-gray-900">
#{order.orderNumber}
</div>
<div className="text-sm text-gray-500">
{formatAustralianDate(order.dateOrdered)}
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">
{order.customer?.name || 'Unknown'}
</div>
</td>
<td className="px-6 py-4">
<div className="text-sm text-gray-900">
{order.items && order.items.length > 0 ? (
order.items.map((item, index) => (
<div key={index} className="mb-1">
<span className="font-medium">{item.title}</span>
<span className="text-gray-500 ml-2">× {item.quantity}</span>
</div>
))
) : (
<span className="text-gray-500 italic">No items listed</span>
)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
${order.total.toFixed(2)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
${printingCost.toFixed(2)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<span className={profit >= 0 ? 'text-green-600' : 'text-red-600'}>
${profit.toFixed(2)}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm">
<span className={`font-medium ${margin >= 20 ? 'text-green-600' : margin >= 10 ? 'text-yellow-600' : 'text-red-600'}`}>
{margin.toFixed(1)}%
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div className="flex justify-end space-x-2">
<button
onClick={() => handleEditOrder(order)}
className="text-indigo-600 hover:text-indigo-900 transition-colors"
title="Edit Order"
>
<Edit className="h-4 w-4" />
</button>
<button
onClick={() => order._id ? handleDeleteClick(order._id) : toast.error('Cannot delete order: Missing ID')}
className={`transition-colors ${
order._id
? 'text-red-600 hover:text-red-900'
: 'text-gray-400 cursor-not-allowed'
}`}
title={order._id ? "Delete Order" : "Cannot delete: Missing ID"}
disabled={!order._id}
>
<Trash2 className="h-4 w-4" />
</button>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
)}
</div>
{/* Edit Order Modal */}
{showEditModal && editingOrder && (
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
<div className="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
<div className="mt-3">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-medium text-gray-900">Edit Order</h3>
<button
onClick={() => setShowEditModal(false)}
className="text-gray-400 hover:text-gray-600"
>
<X className="h-5 w-5" />
</button>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700">Order Number</label>
<input
type="text"
value={editingOrder.orderNumber}
onChange={(e) => setEditingOrder({...editingOrder, orderNumber: e.target.value})}
className="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2 text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Customer Name</label>
<input
type="text"
value={editingOrder.customer?.name || ''}
onChange={(e) => setEditingOrder({
...editingOrder,
customer: { ...editingOrder.customer!, name: e.target.value }
})}
className="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2 text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Total Amount</label>
<input
type="number"
step="0.01"
value={editingOrder.total}
onChange={(e) => setEditingOrder({...editingOrder, total: parseFloat(e.target.value) || 0})}
className="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2 text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Status</label>
<select
value={editingOrder.status}
onChange={(e) => setEditingOrder({...editingOrder, status: e.target.value as Order['status']})}
className="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2 text-sm"
>
<option value="pending">Pending</option>
<option value="processing">Processing</option>
<option value="shipped">Shipped</option>
<option value="delivered">Delivered</option>
<option value="cancelled">Cancelled</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Order Date</label>
<input
type="date"
value={editingOrder.dateOrdered ? new Date(editingOrder.dateOrdered).toISOString().split('T')[0] : ''}
onChange={(e) => setEditingOrder({...editingOrder, dateOrdered: new Date(e.target.value).toISOString()})}
className="mt-1 block w-full border border-gray-300 rounded-md px-3 py-2 text-sm"
/>
</div>
</div>
<div className="flex justify-end space-x-3 mt-6">
<button
onClick={() => setShowEditModal(false)}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-200 rounded-md hover:bg-gray-300"
>
Cancel
</button>
<button
onClick={() => handleSaveOrder(editingOrder)}
className="px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700"
>
Save Changes
</button>
</div>
</div>
</div>
</div>
)}
{/* Delete Confirmation Modal */}
{showDeleteConfirm && (
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
<div className="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
<div className="mt-3 text-center">
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100">
<Trash2 className="h-6 w-6 text-red-600" />
</div>
<h3 className="text-lg font-medium text-gray-900 mt-2">Delete Order</h3>
<div className="mt-2 px-7 py-3">
<p className="text-sm text-gray-500">
Are you sure you want to delete this order? This action cannot be undone.
</p>
</div>
<div className="flex justify-center space-x-3 mt-4">
<button
onClick={handleCancelDelete}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-200 rounded-md hover:bg-gray-300"
>
Cancel
</button>
<button
onClick={handleConfirmDelete}
className="px-4 py-2 text-sm font-medium text-white bg-red-600 rounded-md hover:bg-red-700"
>
Delete
</button>
</div>
</div>
</div>
</div>
)}
{/* Manual Order Creation Modal */}
{showAddModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white p-6 rounded-lg shadow-xl max-w-4xl w-full mx-4 max-h-[90vh] overflow-y-auto">
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-bold text-gray-900">Add Facebook Marketplace Order</h2>
<button
onClick={() => setShowAddModal(false)}
className="text-gray-400 hover:text-gray-600"
>
<X className="w-6 h-6" />
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
{/* Order Details */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Order Number</label>
<input
type="text"
value={newOrder.orderNumber}
onChange={(e) => setNewOrder({...newOrder, orderNumber: e.target.value})}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
placeholder="FB-12345"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Order Date</label>
<input
type="date"
value={newOrder.dateOrdered}
onChange={(e) => setNewOrder({...newOrder, dateOrdered: e.target.value})}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Customer Name</label>
<input
type="text"
value={newOrder.customer.name}
onChange={(e) => setNewOrder({...newOrder, customer: {...newOrder.customer, name: e.target.value}})}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
placeholder="Customer Name"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Customer Email (optional)</label>
<input
type="email"
value={newOrder.customer.email}
onChange={(e) => setNewOrder({...newOrder, customer: {...newOrder.customer, email: e.target.value}})}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
placeholder="customer@email.com"
/>
</div>
</div>
{/* Order Items */}
<div className="mb-6">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-semibold text-gray-900">Order Items</h3>
<button
onClick={handleAddItem}
className="flex items-center gap-2 px-3 py-1 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-sm"
>
<Plus className="w-4 h-4" />
Add Item
</button>
</div>
<div className="space-y-3">
{newOrder.items.map((item, index) => {
const matchingProduct = products?.find(p => p.title.toLowerCase() === item.title.toLowerCase());
return (
<div key={`item-${index}-${item.title || 'empty'}`} className="p-3 bg-gray-50 rounded-lg">
<div className="flex gap-3 items-center mb-2">
<div className="flex-1">
<label className="block text-xs font-medium text-gray-600 mb-1">Product</label>
<div className="flex gap-2">
<select
value={item.title}
onChange={(e) => {
if (e.target.value === '__CREATE_NEW__') {
handleCreateNewProduct('');
} else {
const selectedProduct = products?.find(p => p.title === e.target.value);
updateNewOrderItem(index, 'title', e.target.value);
if (selectedProduct) {
// Auto-fill price if available
if (selectedProduct.price && selectedProduct.price > 0) {
updateNewOrderItem(index, 'price', selectedProduct.price);
}
}
}
}}
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
>
<option value="">Select a product...</option>
{products?.map(product => (
<option key={product._id} value={product.title}>
{product.title} {product.price ? `($${product.price.toFixed(2)})` : ''}
</option>
))}
<option value="__CREATE_NEW__">+ Create New Product</option>
</select>
</div>
</div>
<div className="w-24">
<label className="block text-xs font-medium text-gray-600 mb-1">Qty</label>
<input
type="number"
value={item.quantity}
onChange={(e) => updateNewOrderItem(index, 'quantity', parseInt(e.target.value) || 1)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 text-center"
min="1"
/>
</div>
<div className="w-28">
<label className="block text-xs font-medium text-gray-600 mb-1">Sale Price</label>
<input
type="number"
step="0.01"
value={item.price}
onChange={(e) => updateNewOrderItem(index, 'price', parseFloat(e.target.value) || 0)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
placeholder="0.00"
/>
</div>
{newOrder.items.length > 1 && (
<button
onClick={() => handleRemoveItem(index)}
className="text-red-600 hover:text-red-700 ml-2"
>
<X className="w-4 h-4" />
</button>
)}
</div>
{/* Show printing cost info if product is selected */}
{matchingProduct && (
<div className="mt-2 p-2 bg-blue-50 rounded text-sm text-blue-700">
<span className="font-medium">Printing Cost: ${matchingProduct.printingCost?.toFixed(2) || '0.00'}</span>
<span className="ml-4">Profit per item: ${(item.price - (matchingProduct.printingCost || 0)).toFixed(2)}</span>
</div>
)}
</div>
);
})}
</div>
{/* Total Display with Printing Cost Breakdown */}
<div className="mt-4 space-y-3">
<div className="p-3 bg-blue-50 rounded-lg">
<div className="flex justify-between items-center">
<span className="font-semibold text-gray-700">Order Total (Revenue):</span>
<span className="text-xl font-bold text-blue-600">
${newOrder.items.reduce((sum, item) => sum + (item.quantity * item.price), 0).toFixed(2)}
</span>
</div>
</div>
<div className="p-3 bg-orange-50 rounded-lg">
<div className="flex justify-between items-center">
<span className="font-semibold text-gray-700">Total Printing Cost:</span>
<span className="text-lg font-bold text-orange-600">
${newOrder.items.reduce((sum, item) => {
const product = products?.find(p => p.title.toLowerCase() === item.title.toLowerCase());
return sum + (item.quantity * (product?.printingCost || 0));
}, 0).toFixed(2)}
</span>
</div>
</div>
<div className="p-3 bg-green-50 rounded-lg">
<div className="flex justify-between items-center">
<span className="font-semibold text-gray-700">Estimated Profit:</span>
<span className="text-xl font-bold text-green-600">
${(newOrder.items.reduce((sum, item) => sum + (item.quantity * item.price), 0) -
newOrder.items.reduce((sum, item) => {
const product = products?.find(p => p.title.toLowerCase() === item.title.toLowerCase());
return sum + (item.quantity * (product?.printingCost || 0));
}, 0)).toFixed(2)}
</span>
</div>
</div>
</div>
</div>
{/* Action Buttons */}
<div className="flex justify-end gap-3">
<button
onClick={() => setShowAddModal(false)}
className="px-4 py-2 text-gray-600 bg-gray-100 rounded-lg hover:bg-gray-200"
>
Cancel
</button>
<button
onClick={handleSaveNewOrder}
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700"
>
Save Order
</button>
</div>
</div>
</div>
)}
{/* New Product Creation Modal */}
{showNewProductModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white p-6 rounded-lg shadow-xl max-w-md w-full mx-4">
<div className="flex justify-between items-center mb-6">
<h2 className="text-xl font-bold text-gray-900">Create New Product</h2>
<button
onClick={() => setShowNewProductModal(false)}
className="text-gray-400 hover:text-gray-600"
>
<X className="w-6 h-6" />
</button>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Product Name</label>
<input
type="text"
value={newProductData.title}
onChange={(e) => setNewProductData({...newProductData, title: e.target.value})}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
placeholder="Enter product name"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Sale Price</label>
<input
type="number"
step="0.01"
value={newProductData.price}
onChange={(e) => setNewProductData({...newProductData, price: parseFloat(e.target.value) || 0})}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
placeholder="0.00"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Printing Cost</label>
<input
type="number"
step="0.01"
value={newProductData.printingCost}
onChange={(e) => setNewProductData({...newProductData, printingCost: parseFloat(e.target.value) || 0})}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
placeholder="0.00"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Cost of Goods (optional)</label>
<input
type="number"
step="0.01"
value={newProductData.costOfGoods}
onChange={(e) => setNewProductData({...newProductData, costOfGoods: parseFloat(e.target.value) || 0})}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
placeholder="0.00"
/>
</div>
</div>
<div className="flex justify-end gap-3 mt-6">
<button
onClick={() => setShowNewProductModal(false)}
className="px-4 py-2 text-gray-600 bg-gray-100 rounded-lg hover:bg-gray-200"
>
Cancel
</button>
<button
onClick={handleSaveNewProduct}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Create Product
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default Orders;