2import { useEffect, useState, useRef } from "react";
3import { useRouter } from "next/navigation";
4import { apiClient } from "@/lib/client/apiClient";
5import { useStore } from "@/contexts/StoreContext";
6import { Icon } from '@/contexts/IconContext';
7import { useTheme } from '@/contexts/ThemeContext';
17 manufacturerCode?: string;
18 requiresSerial?: boolean;
32 manufacturerCode?: string;
33 serialNumber?: string;
36interface PaymentMethod {
48 accountBalance?: number;
55 billingAddress1?: string;
56 billingAddress2?: string;
58 billingState?: string;
59 billingPostcode?: string;
60 billingCountry?: string;
61 deliveryAddress1?: string;
62 deliveryAddress2?: string;
63 deliveryCity?: string;
64 deliveryState?: string;
65 deliveryPostcode?: string;
66 deliveryCountry?: string;
69export default function SalesPage() {
70 const router = useRouter();
71 const { session } = useStore();
72 const { theme } = useTheme();
73 const user = session?.user;
74 const store = session?.store;
75 const [products, setProducts] = useState<Product[]>([]);
76 const [allProducts, setAllProducts] = useState<Product[]>([]); // Store all products for client-side filtering
77 const [saleItems, setSaleItems] = useState<SaleItem[]>([]);
78 const [searchTerm, setSearchTerm] = useState("");
79 const [barcode, setBarcode] = useState("");
80 const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
81 const [quantity, setQuantity] = useState(1);
82 const [loading, setLoading] = useState(false);
83 const [total, setTotal] = useState(0);
84 const [showPayment, setShowPayment] = useState(false);
85 const barcodeInputRef = useRef<HTMLInputElement>(null);
87 // Default walk-in customer
88 const defaultWalkInCustomer: Customer = {
101 deliveryAddress1: "",
102 deliveryAddress2: "",
105 deliveryPostcode: "",
109 // Customer selection state
110 const [customers, setCustomers] = useState<Customer[]>([]);
111 const [allCustomers, setAllCustomers] = useState<Customer[]>([]); // Store all customers for client-side filtering
112 const [selectedCustomer, setSelectedCustomer] = useState<Customer | null>(defaultWalkInCustomer);
113 const [customerSearch, setCustomerSearch] = useState("");
114 const [showCustomerModal, setShowCustomerModal] = useState(false);
115 const [customerLoading, setCustomerLoading] = useState(false);
116 const [showBillingEdit, setShowBillingEdit] = useState(false);
117 const [showShippingEdit, setShowShippingEdit] = useState(false);
118 const [editingAddress, setEditingAddress] = useState<any>({});
119 const [editingLineItem, setEditingLineItem] = useState<SaleItem | null>(null);
120 const [showLineItemEdit, setShowLineItemEdit] = useState(false);
121 const [tempPrice, setTempPrice] = useState(0);
122 const [tempDiscount, setTempDiscount] = useState(0);
123 const [saleComments, setSaleComments] = useState("");
124 const [internalComments, setInternalComments] = useState("");
125 const [orderNumber, setOrderNumber] = useState("");
126 const [parkedSales, setParkedSales] = useState<any[]>([]);
127 const [showParkedSales, setShowParkedSales] = useState(false);
128 const [showPastSales, setShowPastSales] = useState(false);
129 const [pastSales, setPastSales] = useState<any[]>([]);
130 const [showReceiptOptions, setShowReceiptOptions] = useState(false);
131 const [receiptEmail, setReceiptEmail] = useState("");
132 const [printReceipt, setPrintReceipt] = useState(true);
133 const [showPrinterSearch, setShowPrinterSearch] = useState(false);
134 const [printerSearchTerm, setPrinterSearchTerm] = useState("");
135 const [printerResults, setPrinterResults] = useState<Product[]>([]);
136 const [selectedPrinter, setSelectedPrinter] = useState<Product | null>(null);
137 const [printerCartridges, setPrinterCartridges] = useState<Product[]>([]);
138 const [emailReceipt, setEmailReceipt] = useState(false);
139 const [selectedPaymentMethod, setSelectedPaymentMethod] = useState("");
140 const [showEndOfDay, setShowEndOfDay] = useState(false);
141 const [showSerialModal, setShowSerialModal] = useState(false);
142 const [pendingSerialProduct, setPendingSerialProduct] = useState<Product | null>(null);
143 const [serialNumber, setSerialNumber] = useState("");
144 const [editingSerialPid, setEditingSerialPid] = useState<number | null>(null);
145 const [tempSerialNumber, setTempSerialNumber] = useState("");
146 const [printerCartridgeRelationships, setPrinterCartridgeRelationships] = useState<any[]>([]);
147 const displayChannel = useRef<BroadcastChannel | null>(null);
149 const paymentMethods: PaymentMethod[] = [
150 { name: "Cash", code: "CASH", icon: "💵" },
151 { name: "EFTPOS", code: "EFTPOS", icon: "💳" },
152 { name: "Credit Card", code: "CREDIT", icon: "💳" },
153 { name: "Account", code: "ACCOUNT", icon: "📋" },
154 { name: "Voucher", code: "VOUCHER", icon: "🎫" },
155 { name: "Direct Debit", code: "DIRECTDEBIT", icon: "🏦" },
156 { name: "Afterpay", code: "AFTERPAY", icon: "💰" }
160 loadPrinterCartridgeRelationships();
163 // Debounced search effect
165 if (searchTerm.length >= 2) {
166 const timeoutId = setTimeout(() => {
167 loadProducts(searchTerm);
169 return () => clearTimeout(timeoutId);
173 // Keyboard shortcut: F2 to focus barcode input
175 const handleKeyDown = (e: KeyboardEvent) => {
176 if (e.key === 'F2') {
178 barcodeInputRef.current?.focus();
182 window.addEventListener('keydown', handleKeyDown);
183 return () => window.removeEventListener('keydown', handleKeyDown);
187 const newTotal = saleItems.reduce((sum, item) => sum + item.total, 0);
191 // Initialize customer display broadcast channel
193 if (typeof BroadcastChannel !== 'undefined') {
194 displayChannel.current = new BroadcastChannel('pos-display');
197 displayChannel.current?.close();
201 // Broadcast cart updates to customer display
203 if (displayChannel.current) {
204 if (saleItems.length > 0) {
205 const subtotal = saleItems.reduce((sum, item) => sum + item.total, 0);
206 const tax = subtotal * 0.1; // Adjust tax rate as needed
208 displayChannel.current.postMessage({
210 cart: saleItems.map(item => ({
219 customer: selectedCustomer?.cid !== 0 ? { name: selectedCustomer?.name } : null
222 displayChannel.current.postMessage({ type: 'clear' });
225 }, [saleItems, total, selectedCustomer]);
227 // Client-side product filtering for instant results
229 if (searchTerm.length === 0) {
231 } else if (searchTerm.length >= 2 && allProducts.length > 0) {
232 // Instant client-side fuzzy search with normalized matching and keyword splitting
233 const normalizeString = (str: string) => str.toLowerCase().replace(/[-\s]/g, '');
234 const keywords = searchTerm.toLowerCase().split(/\s+/).filter(k => k.length > 0);
236 console.log(`Searching for keywords: ${keywords.join(', ')} in ${allProducts.length} products`);
238 const filtered = allProducts.filter(p => {
239 // Combine all searchable fields
240 const searchableText = [
244 p.manufacturerCode || ''
245 ].join(' ').toLowerCase();
247 const normalizedText = normalizeString(searchableText);
249 // Check if ALL keywords are present (in any order)
250 const matches = keywords.every(keyword => {
251 const normalizedKeyword = normalizeString(keyword);
252 return normalizedText.includes(normalizedKeyword);
258 console.log(`Found ${filtered.length} matches`);
259 setProducts(filtered.slice(0, 50)); // Limit to 50 results for performance
261 }, [searchTerm, allProducts]);
263 // Auto-load customers when user starts typing (lazy loading)
265 if (customerSearch.length >= 2 && allCustomers.length === 0 && !customerLoading) {
266 console.log('Customer search triggered - loading all customers for filtering');
269 }, [customerSearch]);
271 // Client-side instant customer filtering
273 if (customerSearch.length === 0) {
275 } else if (customerSearch.length >= 2 && allCustomers.length > 0) {
276 const normalize = (str: string) => str.replace(/[-\s]/g, '').toLowerCase();
277 const filtered = allCustomers.filter(c => {
278 const searchableText = normalize(
279 `${c.name} ${c.email || ''} ${c.phone || ''} ${c.mobile || ''} ${c.city || ''} ${c.postcode || ''}`
281 const keywords = customerSearch.toLowerCase().split(' ').filter(k => k.length > 0);
282 const normalizedKeywords = keywords.map(normalize);
283 return normalizedKeywords.every(keyword => searchableText.includes(keyword));
285 setCustomers(filtered.slice(0, 50)); // Limit to 50 results for performance
287 }, [customerSearch, allCustomers]);
289 // Client-side instant printer filtering - only show actual printers
291 if (printerSearchTerm.length === 0) {
292 setPrinterResults([]);
293 } else if (printerSearchTerm.length >= 2 && allProducts.length > 0) {
294 const normalize = (str: string) => str.replace(/[-\s]/g, '').toLowerCase();
295 const filtered = allProducts.filter(p => {
296 // First check if department ID is 6 (printers department based on product 30004)
297 // Or check if department/category name contains printer
298 const deptCat = `${p.department || ''} ${p.category || ''}`.toLowerCase();
299 const isPrinterByDepartment = Number(p.department) === 6; // Department 6 appears to be printers
300 const isPrinterByCategory = /\b(printer|printers)\b/i.test(deptCat);
302 // Also check product name/description (expanded categories)
303 const nameAndDesc = `${p.name} ${p.description || ''}`.toLowerCase();
304 const isPrinterByKeyword = /\b(printer|printers|multifunction|multi-function|all-in-one|mfp|inkjet|laser|copier|scanner|photocopier)\b/i.test(nameAndDesc) ||
305 // Also check for common printer model patterns (brand + model number)
306 /\b(hp|canon|brother|epson|kyocera|lexmark|samsung|dell|xerox|ricoh|sharp|konica|minolta|oki|pantum)\s*[a-z]?\d{3,5}/i.test(p.name);
308 const isPrinter = isPrinterByDepartment || isPrinterByCategory || isPrinterByKeyword;
309 if (!isPrinter) return false;
311 // Then check if it matches the search term
312 const searchableText = normalize(
313 `${p.name} ${p.description || ''} ${p.barcode || ''} ${p.manufacturerCode || ''}`
315 const keywords = printerSearchTerm.toLowerCase().split(' ').filter(k => k.length > 0);
316 const normalizedKeywords = keywords.map(normalize);
317 return normalizedKeywords.every(keyword => searchableText.includes(keyword));
319 console.log(`Printer search for "${printerSearchTerm}" found ${filtered.length} results`);
320 if (filtered.length > 0) {
321 console.log('Sample printer departments:', filtered.slice(0, 5).map(p => ({ name: p.name, dept: p.department })));
323 setPrinterResults(filtered.slice(0, 50));
325 }, [printerSearchTerm, allProducts]);
327 // Keyboard shortcuts
329 const handleKeyPress = (e: KeyboardEvent) => {
330 // F2 - Select Customer
331 if (e.key === 'F2') {
333 setShowCustomerModal(true);
337 else if (e.key === 'F8') {
339 if (saleItems.length > 0) voidSale();
342 else if (e.key === 'F9') {
344 if (saleItems.length > 0) parkSale();
347 else if (e.key === 'F10') {
349 if (parkedSales.length > 0) setShowParkedSales(true);
353 window.addEventListener('keydown', handleKeyPress);
354 return () => window.removeEventListener('keydown', handleKeyPress);
355 }, [saleItems, parkedSales]);
357 async function loadPrinterCartridgeRelationships() {
359 const response = await fetch('/api/v1/printers-cartridges');
360 const result = await response.json();
362 if (result.success && result.data) {
363 setPrinterCartridgeRelationships(result.data);
364 console.log('Loaded printer-cartridge relationships:', result.data.length);
367 console.error('Failed to load printer-cartridge relationships:', error);
371 async function loadProducts(searchQuery?: string, barcodeQuery?: string) {
375 // Use apiClient for products with OpenAPI - load all products for client-side filtering
376 const params: any = { limit: 10000, source: 'openapi' };
377 if (searchQuery) params.search = searchQuery;
378 if (barcodeQuery) params.barcode = barcodeQuery;
385 const result: ApiResult = await apiClient.getProducts(params);
387 console.log('API result:', result);
389 if (result.success && result.data) {
390 // OpenAPI returns data wrapped in { data: { Product: [...] } } or OData format { value: [...], '@odata.count': ... }
391 const productsData = Array.isArray(result.data)
393 : result.data?.value || result.data?.data?.Product || result.data?.Product || result.data;
395 console.log('Products data:', productsData);
397 if (Array.isArray(productsData) && productsData.length > 0) {
398 // Map OpenAPI fields to our Product interface - only include products with valid data
399 const mappedProducts = productsData
400 .filter((p: any) => p.Pid && (p.Description || p.Name) && (p.Price !== undefined || p.SellPrice !== undefined)) // Only include products with essential data
403 name: p.Description || p.Name || p.name,
404 barcode: p.Barcode || p.barcode || p.Plu,
405 sellprice: parseFloat(p.Price ?? p.SellPrice ?? p.sellprice ?? p.Price1 ?? 0),
406 description: p.Description || p.description,
407 stocklevel: p.StockLevel || p.stocklevel || p.Qty,
408 cost: parseFloat(p.Cost ?? p.cost ?? p.CostPrice ?? 0),
409 manufacturerCode: p.ManufacturerCode || p.manufacturerCode || p.MfrCode || p.SupplierCode,
410 requiresSerial: p.RequiresSerial || p.requiresSerial || false,
411 department: p.Department || p.department || p.Dept,
412 category: p.Category || p.category || p.Cat
414 setAllProducts(mappedProducts); // Store all products
415 console.log(`Loaded ${mappedProducts.length} products for client-side search`);
416 } else if (Array.isArray(productsData) && productsData.length === 0) {
417 console.log('API returned empty product list');
420 console.warn('Unexpected products data format (not an array):', typeof productsData, productsData);
424 console.error("API call failed or no data:", result);
427 console.error("Failed to load products:", error);
433 function addToSale(product: Product, qty = quantity) {
434 if (!product || !product.pid) {
435 console.error('Invalid product', product);
439 // Check if product requires serial number
440 if (product.requiresSerial && qty === 1) {
441 setPendingSerialProduct(product);
442 setShowSerialModal(true);
446 const existingItem = saleItems.find(item => item.pid === product.pid);
449 setSaleItems(items =>
451 item.pid === product.pid
452 ? { ...item, qty: item.qty + qty, total: (item.qty + qty) * item.price }
457 const newItem: SaleItem = {
461 price: product.sellprice,
462 total: qty * product.sellprice,
464 manufacturerCode: product.manufacturerCode,
467 setSaleItems(items => [...items, newItem]);
472 setSelectedProduct(null);
475 function addToSaleWithSerial(serial: string) {
476 if (!pendingSerialProduct) return;
478 const newItem: SaleItem = {
479 pid: pendingSerialProduct.pid,
480 name: pendingSerialProduct.name,
482 price: pendingSerialProduct.sellprice,
483 total: pendingSerialProduct.sellprice,
484 serialNumber: serial,
485 cost: pendingSerialProduct.cost,
486 manufacturerCode: pendingSerialProduct.manufacturerCode,
490 setSaleItems(items => [...items, newItem]);
493 setSelectedProduct(null);
494 setShowSerialModal(false);
495 setPendingSerialProduct(null);
499 function updateSerialNumber(pid: number, serial: string) {
500 setSaleItems(items =>
503 ? { ...item, serialNumber: serial }
507 setEditingSerialPid(null);
508 setTempSerialNumber('');
511 function startEditingSerial(pid: number, currentSerial?: string) {
512 setEditingSerialPid(pid);
513 setTempSerialNumber(currentSerial || '');
516 function removeFromSale(pid: number) {
517 setSaleItems(items => items.filter(item => item.pid !== pid));
520 function updateQuantity(pid: number, newQty: number) {
526 setSaleItems(items =>
529 ? { ...item, qty: newQty, total: (newQty * item.price) - ((newQty * item.price * item.discount) / 100) }
535 async function loadCustomers(searchQuery?: string) {
537 setCustomerLoading(true);
539 const params: any = { limit: 10000, source: 'openapi' }; // Load all customers for client-side filtering
546 const result: ApiResult = await apiClient.getCustomers(params);
548 console.log('Customer API response:', result);
550 if (result.success && result.data) {
551 const customersData = Array.isArray(result.data)
553 : result.data?.data?.Customer || result.data;
555 if (Array.isArray(customersData)) {
556 console.log(`Loaded ${customersData.length} customers for client-side filtering`);
557 const mappedCustomers = customersData
558 .filter((c: any) => c.Cid && c.Name)
561 name: c.Name || c.name,
562 email: c.Email || c.email,
563 phone: c.Phone || c.phone,
564 mobile: c.Mobile || c.mobile,
565 accountBalance: c.AccountBalance || c.accountBalance || 0,
566 address1: c.Address1 || c.address1,
567 address2: c.Address2 || c.address2,
568 city: c.City || c.city,
569 state: c.State || c.state,
570 postcode: c.Postcode || c.postcode,
571 country: c.Country || c.country,
572 deliveryAddress1: c.DeliveryAddress1 || c.deliveryAddress1,
573 deliveryAddress2: c.DeliveryAddress2 || c.deliveryAddress2,
574 deliveryCity: c.DeliveryCity || c.deliveryCity,
575 deliveryState: c.DeliveryState || c.deliveryState,
576 deliveryPostcode: c.DeliveryPostcode || c.deliveryPostcode,
577 deliveryCountry: c.DeliveryCountry || c.deliveryCountry
579 setAllCustomers(mappedCustomers); // Store all customers
580 // Don't set customers here - let the useEffect handle filtering based on search
582 console.warn('Customer API returned empty or invalid data:', customersData);
585 console.warn('Customer API call unsuccessful or no data:', result);
588 console.error('Failed to load customers:', error);
590 setCustomerLoading(false);
594 function selectCustomer(customer: Customer | null) {
595 setSelectedCustomer(customer);
596 setShowCustomerModal(false);
597 setCustomerSearch("");
600 function openLineItemEdit(item: SaleItem) {
601 setEditingLineItem(item);
602 setTempPrice(item.price);
603 setTempDiscount(item.discount || 0);
604 setShowLineItemEdit(true);
607 function saveLineItemEdit() {
608 if (!editingLineItem) return;
610 setSaleItems(items =>
612 item.pid === editingLineItem.pid
616 discount: tempDiscount,
617 total: (item.qty * tempPrice) - ((item.qty * tempPrice * tempDiscount) / 100)
622 setShowLineItemEdit(false);
623 setEditingLineItem(null);
626 function toggleReturn(pid: number) {
627 setSaleItems(items =>
630 ? { ...item, isReturn: !item.isReturn }
636 function parkSale() {
637 if (saleItems.length === 0) {
638 alert('No items to park');
643 id: `PARK_${Date.now()}`,
645 customer: selectedCustomer,
648 comments: saleComments,
650 timestamp: new Date().toISOString()
653 setParkedSales([...parkedSales, parkedSale]);
655 alert('Sale parked successfully');
658 function unparkSale(parkedSale: any) {
659 setSaleItems(parkedSale.items);
660 setSelectedCustomer(parkedSale.customer);
661 setOrderNumber(parkedSale.orderNumber || '');
662 setSaleComments(parkedSale.comments || '');
663 setInternalComments(parkedSale.internalComments || '');
664 setParkedSales(parkedSales.filter(s => s.id !== parkedSale.id));
665 setShowParkedSales(false);
666 alert('Sale retrieved successfully');
669 function voidSale() {
670 if (saleItems.length === 0) {
671 alert('No sale to void');
675 if (confirm('Are you sure you want to VOID this sale? This cannot be undone.')) {
677 alert('Sale voided');
681 async function loadPastSales() {
684 const result = await apiClient.getSales({ limit: 50 });
685 if (result.success && result.data) {
686 const salesData = Array.isArray(result.data) ? result.data : (result.data as any)?.data?.Sale || [];
687 setPastSales(salesData);
690 console.error('Failed to load past sales:', error);
696 function clearSale() {
699 setShowPayment(false);
700 setSelectedCustomer(defaultWalkInCustomer);
701 setCustomerSearch("");
703 setInternalComments("");
707 async function processSale(paymentMethod: string) {
708 if (saleItems.length === 0) {
709 alert('No items in sale');
713 // Show receipt options first
714 if (!showReceiptOptions) {
715 setReceiptEmail(selectedCustomer?.email || '');
716 setSelectedPaymentMethod(paymentMethod);
717 setShowReceiptOptions(true);
724 // Create sale object for Fieldpine API format
725 const saleData: any = {
726 ExternalId: `POS_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
727 CompletedDt: new Date().toISOString(),
728 Phase: 1, // 1 = completed sale
729 OrderNo: orderNumber || undefined,
730 Comments: saleComments || undefined,
731 InternalComments: internalComments || undefined,
732 CompletedBy: user?.name || 'Unknown',
733 Teller: user?.name || 'Unknown',
735 StoreName: store?.name,
736 ReceiptEmail: emailReceipt ? receiptEmail : undefined,
737 PrintReceipt: printReceipt,
738 LINE: saleItems.map(item => ({
741 TotalPrice: item.total
744 Type: paymentMethod.toLowerCase(),
749 // Add customer if selected
750 if (selectedCustomer) {
751 saleData.Cid = selectedCustomer.cid;
752 saleData.CustomerName = selectedCustomer.name;
755 // Use API helper to process sale
756 const result = await apiClient.createSale(saleData);
758 if (result.success) {
759 const customerInfo = selectedCustomer ? `\nCustomer: ${selectedCustomer.name}` : '';
760 const receiptInfo = emailReceipt ? `\nReceipt emailed to: ${receiptEmail}` : '';
761 alert(`✅ Sale completed successfully!\n\nTotal: $${total.toFixed(2)}\nPayment: ${paymentMethod}${customerInfo}${receiptInfo}\nSale ID: ${result.data?.saleId || 'N/A'}`);
762 setShowReceiptOptions(false);
765 console.error("Sale processing failed:", result.error);
766 alert(`❌ Sale processing failed: ${result.error || 'Please try again.'}`);
769 console.error("Sale processing failed:", error);
770 alert(`❌ Sale processing failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
777 <div className="min-h-screen p-2 sm:p-4 md:p-8 h-full overflow-y-auto bg-bg">
778 <div className="max-w-7xl mx-auto space-y-3 sm:space-y-4 md:space-y-6">
779 {/* Modern Top Action Bar with Enhanced UI */}
780 <div className="bg-surface text-text shadow-xl p-3 sm:p-4 md:p-5 rounded-lg sm:rounded-xl md:rounded-2xl border border-border">
781 <div className="flex flex-col lg:flex-row items-stretch lg:items-center justify-between gap-3 sm:gap-4">
782 <div className="flex gap-2 sm:gap-3 flex-wrap">
785 // Route merchant users to sales reports hub, store users to POS sales report
786 const destination = store?.type === 'management'
787 ? '/pages/reports/sales-reports'
788 : '/pages/reports/pos-sales-report';
789 router.push(destination);
791 className="px-2.5 sm:px-4 md:px-5 py-1.5 sm:py-2 md:py-2.5 bg-brand text-surface rounded-lg sm:rounded-xl hover:opacity-90 active:transform active:scale-95 transition-all duration-150 text-xs sm:text-sm font-semibold shadow-lg hover:shadow-xl flex items-center gap-1 sm:gap-2"
793 <Icon name="assessment" size={16} className="sm:text-base" /> <span className="hidden sm:inline">Past Sales</span><span className="sm:hidden">Past</span>
796 onClick={() => setShowEndOfDay(true)}
797 className="px-2.5 sm:px-4 md:px-5 py-1.5 sm:py-2 md:py-2.5 bg-info text-surface rounded-lg sm:rounded-xl hover:opacity-90 active:transform active:scale-95 transition-all duration-150 text-xs sm:text-sm font-semibold shadow-lg hover:shadow-xl flex items-center gap-1 sm:gap-2"
799 <Icon name="work" size={16} className="sm:text-base" /> <span className="hidden md:inline">End of Day</span><span className="md:hidden">EOD</span>
803 disabled={saleItems.length === 0}
804 className="px-2.5 sm:px-4 md:px-5 py-1.5 sm:py-2 md:py-2.5 bg-warn text-surface rounded-lg sm:rounded-xl hover:opacity-90 active:transform active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-150 text-xs sm:text-sm font-semibold shadow-lg hover:shadow-xl flex items-center gap-1 sm:gap-2"
806 <Icon name="pause" size={16} className="sm:text-base" /> <span className="hidden sm:inline">Park</span>
809 onClick={() => setShowParkedSales(true)}
810 disabled={parkedSales.length === 0}
811 className="px-2.5 sm:px-4 md:px-5 py-1.5 sm:py-2 md:py-2.5 bg-success text-surface rounded-lg sm:rounded-xl hover:opacity-90 active:transform active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-150 text-xs sm:text-sm font-semibold shadow-lg hover:shadow-xl flex items-center gap-1 sm:gap-2"
813 <Icon name="play_arrow" size={16} className="sm:text-base" /> <span className="hidden sm:inline">Unpark</span>
817 disabled={saleItems.length === 0}
818 className="px-2.5 sm:px-4 md:px-5 py-1.5 sm:py-2 md:py-2.5 bg-danger text-surface rounded-lg sm:rounded-xl hover:opacity-90 active:transform active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-150 text-xs sm:text-sm font-semibold shadow-lg hover:shadow-xl flex items-center gap-1 sm:gap-2"
820 <Icon name="close" size={16} className="sm:text-base" /> <span className="hidden sm:inline">Void</span>
823 className="px-2.5 sm:px-4 md:px-5 py-1.5 sm:py-2 md:py-2.5 bg-info text-surface rounded-lg sm:rounded-xl hover:opacity-90 active:transform active:scale-95 transition-all duration-150 text-xs sm:text-sm font-semibold shadow-lg hover:shadow-xl flex items-center gap-1 sm:gap-2"
825 <Icon name="smartphone" size={16} className="sm:text-base" /> <span className="hidden lg:inline">Manual Barcode</span><span className="lg:hidden">Barcode</span>
830 '/pages/sales/display',
832 'width=1024,height=768'
835 className="px-2.5 sm:px-4 md:px-5 py-1.5 sm:py-2 md:py-2.5 bg-brand2 text-text rounded-lg sm:rounded-xl hover:opacity-90 active:transform active:scale-95 transition-all duration-150 text-xs sm:text-sm font-semibold shadow-lg hover:shadow-xl flex items-center gap-1 sm:gap-2"
837 <Icon name="desktop_windows" size={16} className="sm:text-base" /> <span className="hidden lg:inline">Customer Display</span><span className="lg:hidden">Display</span>
843 {/* Page Title with Modern Styling */}
844 <div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2 sm:gap-4">
845 <h1 className="text-2xl sm:text-3xl md:text-4xl font-extrabold bg-gradient-to-r from-brand to-brand2 bg-clip-text text-transparent flex items-center gap-2 sm:gap-3">
846 <Icon name="shopping_cart" size={36} className="text-brand" /> <span className="hidden sm:inline">Point of Sale</span><span className="sm:hidden">POS</span>
848 <div className="text-xs sm:text-sm text-muted font-medium">
849 <span className="hidden md:inline">{new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}</span>
850 <span className="md:hidden">{new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}</span>
854 <div className="space-y-3 sm:space-y-4 md:space-y-6">
855 {/* Customer Section - Modern Card Design */}
856 <div className="w-full bg-surface shadow-2xl p-4 sm:p-6 md:p-8 rounded-lg sm:rounded-xl md:rounded-2xl border border-border">
857 <div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 sm:gap-4 md:gap-6 mb-4 sm:mb-5 md:mb-6">
858 <h2 className="text-xl sm:text-2xl md:text-3xl font-bold text-text flex items-center gap-2 sm:gap-3" id="custname">
859 {selectedCustomer ? <Icon name="person" size={36} /> : <Icon name="search" size={36} />}
860 <span className="break-all">{selectedCustomer?.name || 'Select Customer'}</span>
862 {selectedCustomer && (
864 onClick={() => setSelectedCustomer(null)}
865 className="w-full sm:w-auto px-3 sm:px-4 py-2 bg-brand text-surface rounded-lg hover:opacity-90 active:transform active:scale-95 transition-all text-xs sm:text-sm font-semibold shadow-md hover:shadow-lg flex items-center justify-center gap-2"
867 <Icon name="sync" size={16} /> Change Customer
872 {/* Customer Search Bar */}
873 {!selectedCustomer && (
874 <div className="mb-4">
875 <div className="flex gap-3">
876 <div className="flex-1 relative">
877 <div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
878 <Icon name="search" size={20} className="text-muted" />
882 value={customerSearch}
884 setCustomerSearch(e.target.value);
887 if (e.key === 'Escape') {
888 setCustomerSearch('');
892 placeholder="Search customers by name, email, or phone... (Press F2 or ESC to clear)"
893 className="w-full pl-12 pr-4 py-3 border-2 border-border rounded-xl focus:ring-2 focus:ring-brand focus:border-brand focus:outline-none transition-all text-base shadow-sm"
899 setCustomerSearch('');
902 className="px-5 py-3 bg-surface-2 hover:opacity-90 rounded-xl transition-all font-medium text-text shadow-sm active:transform active:scale-95"
904 <Icon name="close" size={20} /> Clear
909 {/* Customer Results Dropdown */}
910 {(customerSearch.length > 0 || customers.length > 0) && (
911 <div className="mt-3 border-2 border-border rounded-xl bg-surface shadow-2xl max-h-80 overflow-y-auto">
913 <div className="p-6 text-center">
914 <div className="inline-block animate-spin rounded-full h-8 w-8 border-4 border-brand border-t-transparent"></div>
915 <div className="mt-3 text-text font-medium">Loading customers...</div>
919 {/* Cash Sale - Always First */}
921 onClick={() => selectCustomer({
934 deliveryAddress1: '',
935 deliveryAddress2: '',
938 deliveryPostcode: '',
941 className="p-4 border-b-2 border-border hover:bg-surface-2 cursor-pointer bg-surface-2/50 transition-all group"
943 <div className="font-bold text-brand flex items-center gap-2 text-lg">
944 <Icon name="person" size={20} /> Cash Sale
945 <span className="ml-auto text-sm bg-brand/20 text-brand px-3 py-1 rounded-full group-hover:bg-brand/30 transition-colors">Quick Select</span>
947 <div className="text-sm text-muted mt-1">No customer account needed - Perfect for casual purchases</div>
950 {/* Search Results */}
951 {customers.map((customer) => (
954 onClick={() => selectCustomer(customer)}
955 className="p-4 border-b border-slate-100 hover:bg-gradient-to-r hover:from-blue-50 hover:to-indigo-50 cursor-pointer transition-all"
957 <div className="font-bold text-text text-lg">{customer.name}</div>
958 <div className="flex gap-4 mt-1 flex-wrap">
959 {customer.email && <div className="text-sm text-muted flex items-center gap-1"><Icon name="email" size={16} />{customer.email}</div>}
960 {customer.phone && <div className="text-sm text-muted flex items-center gap-1"><Icon name="phone" size={16} />{customer.phone}</div>}
961 {customer.accountBalance !== undefined && (
962 <div className="text-sm text-muted flex items-center gap-1">
963 <Icon name="credit_card" size={16} />Balance: ${customer.accountBalance.toFixed(2)}
976 {selectedCustomer && (
977 <div className="mt-4 sm:mt-5 md:mt-6 space-y-4 sm:space-y-5 md:space-y-6">
978 {/* Address Cards Container */}
979 <div className="grid grid-cols-1 lg:grid-cols-2 gap-4 sm:gap-5 md:gap-6">
980 {/* Billing Address Card */}
981 <div className="bg-surface-2 border-2 border-border rounded-xl p-6 shadow-lg">
982 <div className="flex justify-between items-center mb-4">
983 <h3 className="text-lg font-bold text-text flex items-center gap-2">
984 <Icon name="description" size={20} /> Billing Address
989 setEditingAddress({...selectedCustomer});
990 setShowBillingEdit(true);
992 className="px-4 py-2 bg-brand hover:opacity-90 text-surface rounded-lg text-sm font-semibold cursor-pointer transition-all shadow-md hover:shadow-lg active:transform active:scale-95 flex items-center gap-2"
994 <Icon name="edit" size={16} /> Edit
997 <div className="bg-surface p-4 rounded-lg border border-border shadow-sm">
998 <div className="font-bold text-text text-lg mb-2">{selectedCustomer.name}</div>
999 <div className="space-y-1 text-text">
1000 {selectedCustomer.billingAddress1 && <div className="flex items-start gap-2"><Icon name="location_on" size={16} className="text-brand" />{selectedCustomer.billingAddress1}</div>}
1001 {selectedCustomer.billingAddress2 && <div className="pl-6">{selectedCustomer.billingAddress2}</div>}
1002 {(selectedCustomer.billingCity || selectedCustomer.billingState || selectedCustomer.billingPostcode) && (
1003 <div className="flex items-center gap-2">
1004 <Icon name="location_city" size={16} className="text-brand" />
1005 {[selectedCustomer.billingCity, selectedCustomer.billingState, selectedCustomer.billingPostcode].filter(Boolean).join(', ')}
1008 {selectedCustomer.billingCountry && <div className="flex items-center gap-2"><Icon name="public" size={16} className="text-brand" />{selectedCustomer.billingCountry}</div>}
1009 {!selectedCustomer.billingAddress1 && !selectedCustomer.billingCity && (
1010 <div className="text-muted italic">No billing address on file</div>
1016 {/* Delivery Address Card */}
1017 <div className="bg-surface-2 border-2 border-border rounded-xl p-6 shadow-lg">
1018 <div className="flex justify-between items-center mb-4">
1019 <h3 className="text-lg font-bold text-text flex items-center gap-2">
1020 <Icon name="local_shipping" size={20} /> Delivery Address
1025 setEditingAddress({...selectedCustomer});
1026 setShowShippingEdit(true);
1028 className="px-4 py-2 bg-success hover:opacity-90 text-surface rounded-lg text-sm font-semibold cursor-pointer transition-all shadow-md hover:shadow-lg active:transform active:scale-95 flex items-center gap-2"
1030 <Icon name="edit" size={16} /> Edit
1033 <div className="bg-bg p-4 rounded-lg border border-border shadow-sm">
1034 <div className="font-bold text-text text-lg mb-2">{selectedCustomer.name}</div>
1035 <div className="space-y-1 text-text">
1036 {selectedCustomer.deliveryAddress1 && <div className="flex items-start gap-2"><Icon name="location_on" size={16} className="text-success" />{selectedCustomer.deliveryAddress1}</div>}
1037 {selectedCustomer.deliveryAddress2 && <div className="pl-6">{selectedCustomer.deliveryAddress2}</div>}
1038 {(selectedCustomer.deliveryCity || selectedCustomer.deliveryState || selectedCustomer.deliveryPostcode) && (
1039 <div className="flex items-center gap-2">
1040 <Icon name="location_city" size={16} className="text-success" />
1041 {[selectedCustomer.deliveryCity, selectedCustomer.deliveryState, selectedCustomer.deliveryPostcode].filter(Boolean).join(', ')}
1044 {selectedCustomer.deliveryCountry && <div className="flex items-center gap-2"><Icon name="public" size={16} className="text-success" />{selectedCustomer.deliveryCountry}</div>}
1045 {!selectedCustomer.deliveryAddress1 && !selectedCustomer.deliveryCity && (
1046 <div className="text-muted italic">No delivery address on file</div>
1053 {/* Order Number */}
1054 <div className="bg-surface border-2 border-border rounded-xl p-5 shadow-md">
1055 <label className="text-sm font-bold text-text uppercase tracking-wide flex items-center gap-2 mb-3">
1056 <Icon name="tag" size={20} /> Order / Reference Number
1061 onChange={(e) => setOrderNumber(e.target.value)}
1062 placeholder="Enter order number or reference..."
1063 className="w-full px-4 py-3 border-2 border-border rounded-xl focus:ring-2 focus:ring-brand focus:border-brand focus:outline-none transition-all text-base shadow-sm"
1067 {/* Sale Comments - 50/50 Column Layout */}
1068 <div className="grid grid-cols-1 lg:grid-cols-2 gap-4 lg:gap-6">
1069 {/* Sale Comments Column */}
1070 <div className="bg-surface-2 p-4 lg:p-5 rounded-xl border-2 border-border">
1071 <label className="text-sm font-bold text-text uppercase tracking-wide flex items-center gap-2 mb-2 lg:mb-3">
1072 <Icon name="chat" size={20} /> <span className="hidden sm:inline">Sale Comments (Customer Visible)</span><span className="sm:hidden">Comments</span>
1075 value={saleComments}
1076 onChange={(e) => setSaleComments(e.target.value)}
1077 className="w-full px-3 py-2 lg:px-4 lg:py-3 border-2 border-border rounded-xl focus:ring-2 focus:ring-brand focus:outline-none transition-all resize-none text-sm"
1079 placeholder="Enter comments visible to the customer..."
1081 <div className="flex gap-2 mt-2 lg:mt-3">
1083 onClick={() => setShowReceiptOptions(true)}
1084 className="w-full px-3 py-2 lg:px-4 bg-brand hover:opacity-90 text-surface rounded-lg transition-all shadow-md hover:shadow-lg text-xs lg:text-sm font-semibold flex items-center justify-center gap-1 lg:gap-2"
1086 <Icon name="receipt" size={16} /> <span className="hidden sm:inline">Receipt Options</span><span className="sm:hidden">Receipt</span>
1091 {/* Internal Comments Column */}
1092 <div className="bg-surface-2 p-4 lg:p-5 rounded-xl border-2 border-border">
1093 <label className="text-sm font-bold text-text uppercase tracking-wide flex items-center gap-2 mb-2 lg:mb-3">
1094 <Icon name="lock" size={20} /> <span className="hidden sm:inline">Internal Comments (Staff Only)</span><span className="sm:hidden">Internal</span>
1097 value={internalComments}
1098 onChange={(e) => setInternalComments(e.target.value)}
1099 className="w-full px-3 py-2 lg:px-4 lg:py-3 border-2 border-border rounded-xl focus:ring-2 focus:ring-warn focus:outline-none transition-all resize-none text-sm"
1101 placeholder="Enter internal notes (not visible to customer)..."
1109 {/* Product Search & Sales Items - Combined Section */}
1110 <div className="w-full bg-surface shadow-2xl p-4 sm:p-6 md:p-8 rounded-lg sm:rounded-xl md:rounded-2xl border border-border">
1111 {/* Product Search & Selection */}
1112 <div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 sm:gap-4 md:gap-6 mb-4 sm:mb-5 md:mb-6">
1113 <h2 className="text-lg sm:text-xl md:text-2xl font-bold text-text flex items-center gap-2 sm:gap-3">
1114 <Icon name="search" size={28} /> Product Lookup
1117 onClick={() => setShowPrinterSearch(true)}
1118 className="w-full sm:w-auto px-3 sm:px-4 md:px-5 py-2 sm:py-2.5 bg-brand2 text-text rounded-lg sm:rounded-xl hover:opacity-90 active:transform active:scale-95 transition-all duration-150 text-xs sm:text-sm font-semibold shadow-lg hover:shadow-xl flex items-center justify-center gap-2"
1120 <Icon name="print" size={20} /> Printers
1124 {/* Search Controls - Modern Design */}
1125 <div className="space-y-3 sm:space-y-4 mb-4 sm:mb-5 md:mb-6">
1126 {/* Product Search Input */}
1127 <div className="relative">
1128 <div className="absolute inset-y-0 left-0 pl-3 sm:pl-4 flex items-center pointer-events-none">
1129 <Icon name="search" size={20} className="text-muted" />
1133 placeholder="Search products..."
1135 onChange={(e) => setSearchTerm(e.target.value)}
1136 className="w-full pl-10 sm:pl-12 pr-3 sm:pr-4 py-2.5 sm:py-3 border-2 border-border rounded-lg sm:rounded-xl focus:ring-2 focus:ring-success focus:border-success focus:outline-none transition-all text-sm sm:text-base shadow-sm"
1140 {/* Barcode Scanner Input */}
1141 <div className="relative">
1142 <div className="absolute inset-y-0 left-0 pl-3 sm:pl-4 flex items-center pointer-events-none">
1143 <Icon name="qr_code_scanner" size={20} className="text-muted" />
1146 ref={barcodeInputRef}
1148 placeholder="Scan barcode..."
1149 className="w-full pl-10 sm:pl-12 pr-3 sm:pr-4 py-2.5 sm:py-3 border-2 border-border rounded-lg sm:rounded-xl focus:ring-2 focus:ring-brand focus:border-brand focus:outline-none transition-all text-sm sm:text-base shadow-sm"
1151 onChange={(e) => setBarcode(e.target.value)}
1152 onKeyDown={async (e) => {
1153 if (e.key === 'Enter' && barcode) {
1155 // Try to find in current products first
1156 let product = products.find(p => p.barcode === barcode);
1158 // If not found, search by barcode via API
1166 const result: ApiResult = await apiClient.getProducts({ barcode, limit: 1, source: 'openapi' });
1167 if (result.success && result.data) {
1168 const productsData = Array.isArray(result.data)
1170 : result.data?.data?.Product || result.data;
1171 if (Array.isArray(productsData) && productsData.length > 0) {
1172 const p = productsData[0];
1173 // Only create product if it has valid data
1174 if (p.Pid && (p.Description || p.Name) && (p.Price !== undefined || p.SellPrice !== undefined)) {
1176 pid: p.Pid || p.pid,
1177 name: p.Description || p.Name || p.name,
1178 barcode: p.Barcode || p.barcode || p.Plu,
1179 sellprice: parseFloat(p.Price ?? p.SellPrice ?? p.sellprice ?? p.Price1 ?? 0),
1180 description: p.Description || p.description,
1181 stocklevel: p.StockLevel || p.stocklevel || p.Qty
1191 setBarcode(''); // Clear barcode after successful scan
1193 alert(`Product not found for barcode: ${barcode}`);
1201 {/* Product Search Results */}
1202 {searchTerm.length >= 2 && (
1203 <div className="mt-4 bg-surface border-2 border-border rounded-xl shadow-xl max-h-96 overflow-y-auto">
1205 <div className="p-6 text-center">
1206 <div className="inline-block animate-spin rounded-full h-8 w-8 border-4 border-brand border-t-transparent"></div>
1207 <div className="mt-3 text-text font-medium">Searching products...</div>
1209 ) : products.length === 0 ? (
1210 <div className="p-6 text-center text-muted">
1211 <Icon name="search" size={64} className="inline-block mb-2" />
1212 <div className="font-semibold">No products found</div>
1213 <div className="text-sm">Try a different search term</div>
1216 <div className="divide-y divide-slate-200">
1217 {products.map((product) => (
1222 setSearchTerm(''); // Clear search after adding
1224 className="w-full p-4 hover:bg-surface-2 transition-colors text-left flex items-center justify-between group"
1226 <div className="flex-1">
1227 <div className="font-semibold text-text group-hover:text-brand transition-colors">
1230 <div className="text-sm text-muted mt-1 flex gap-4">
1231 {product.barcode && (
1232 <span className="flex items-center gap-1">
1233 <Icon name="tag" size={16} /> {product.barcode}
1236 {product.manufacturerCode && (
1237 <span className="flex items-center gap-1">
1238 <Icon name="local_offer" size={16} /> {product.manufacturerCode}
1241 {product.stocklevel !== undefined && (
1242 <span className={`flex items-center gap-1 ${product.stocklevel > 0 ? 'text-success' : 'text-danger'}`}>
1243 <Icon name="inventory_2" size={16} /> Stock: {product.stocklevel}
1248 <div className="text-right ml-4">
1249 <div className="text-xl font-bold text-brand">
1250 ${product.sellprice.toFixed(2)}
1252 <div className="text-xs text-slate-500 mt-1">
1263 {/* Sale Items Table and Payment */}
1264 {/* Sale Items Table - Premium Design */}
1265 {saleItems.length > 0 ? (
1266 <div className="mb-4 sm:mb-5 md:mb-6">
1267 {/* Desktop Table View */}
1268 <div className="hidden lg:block overflow-x-auto border-2 border-slate-200 rounded-xl md:rounded-2xl shadow-xl">
1269 <table className="w-full text-sm">
1270 <thead className="bg-gradient-to-r from-blue-600 to-indigo-600 border-b-4 border-blue-700">
1272 <th className="px-4 py-4 text-left font-bold text-surface">Description</th>
1273 <th className="px-4 py-4 text-left font-bold text-surface">Mfr Part</th>
1274 <th className="px-4 py-4 text-left font-bold text-surface">Serial #</th>
1275 <th className="px-4 py-4 text-center font-bold text-surface">Qty</th>
1276 <th className="px-4 py-4 text-right font-bold text-surface">Each</th>
1277 <th className="px-4 py-4 text-right font-bold text-surface">Disc %</th>
1278 <th className="px-4 py-4 text-right font-bold text-surface">Total</th>
1279 <th className="px-4 py-4 text-right font-bold text-surface">GP $</th>
1280 <th className="px-4 py-4 text-center font-bold text-surface">Actions</th>
1283 <tbody className="bg-surface divide-y divide-border">
1284 {saleItems.map((item, index) => {
1285 const grossProfit = item.cost ? (item.total - (item.cost * item.qty)) : 0;
1286 const discountedPrice = item.discount ? item.price * (1 - item.discount / 100) : item.price;
1289 <tr key={`${item.pid}-${index}`} className={`hover:bg-surface-2 transition-colors ${item.isReturn ? 'bg-danger/10' : index % 2 === 0 ? 'bg-surface/50' : 'bg-surface'}`}>
1290 <td className="px-4 py-4">
1291 <div className="flex items-center gap-3">
1292 <div className="flex-1">
1293 <div className="font-semibold text-text">{item.name}</div>
1295 <span className="inline-flex items-center gap-1 text-xs bg-danger text-surface px-3 py-1 rounded-full font-bold mt-1">
1296 <Icon name="undo" size={14} /> RETURN
1302 <td className="px-4 py-4 text-muted font-mono text-xs">{item.manufacturerCode || '—'}</td>
1303 <td className="px-4 py-4">
1304 {editingSerialPid === item.pid ? (
1307 value={tempSerialNumber}
1308 onChange={(e) => setTempSerialNumber(e.target.value)}
1310 if (e.key === 'Enter') {
1311 updateSerialNumber(item.pid, tempSerialNumber);
1312 } else if (e.key === 'Escape') {
1313 setEditingSerialPid(null);
1314 setTempSerialNumber('');
1318 if (tempSerialNumber.trim()) {
1319 updateSerialNumber(item.pid, tempSerialNumber);
1321 setEditingSerialPid(null);
1322 setTempSerialNumber('');
1325 placeholder="Enter serial..."
1326 className="w-32 px-2 py-1 border-2 border-brand rounded text-xs font-mono font-semibold focus:ring-2 focus:ring-brand focus:outline-none"
1331 onClick={() => startEditingSerial(item.pid, item.serialNumber)}
1332 className={`group inline-flex items-center gap-1 text-xs px-2 py-1 rounded font-mono font-semibold transition-all hover:scale-105 ${
1333 item.serialNumber ? 'serial-btn-active' : 'serial-btn-inactive'
1336 <Icon name="tag" size={14} />
1337 <span>{item.serialNumber || 'Add Serial'}</span>
1338 <Icon name="edit" size={10} className="opacity-0 group-hover:opacity-100 transition-opacity" />
1342 <td className="px-4 py-4">
1343 <div className="flex items-center justify-center">
1347 onChange={(e) => updateQuantity(item.pid, Number(e.target.value))}
1348 className="w-20 px-3 py-2 border-2 border-border rounded-lg text-center font-bold focus:ring-2 focus:ring-brand focus:outline-none"
1352 <td className="px-4 py-4 text-right font-semibold text-text">
1353 ${discountedPrice.toFixed(2)}
1355 <td className="px-4 py-4 text-right">
1356 <span className={`font-semibold ${item.discount > 0 ? 'text-warn' : 'text-muted'}`}>
1357 {item.discount > 0 ? `${item.discount.toFixed(1)}%` : '—'}
1360 <td className="px-4 py-4 text-right font-bold text-lg text-brand">${item.total.toFixed(2)}</td>
1361 <td className="px-4 py-4 text-right">
1362 <span className={`font-bold ${grossProfit >= 0 ? 'text-success' : 'text-danger'}`}>
1363 ${grossProfit.toFixed(2)}
1366 <td className="px-4 py-4">
1367 <div className="flex gap-2 justify-center flex-wrap">
1369 onClick={() => removeFromSale(item.pid)}
1370 className="px-3 py-1.5 bg-danger hover:opacity-90 text-surface rounded-lg text-xs font-semibold transition-all shadow-md hover:shadow-lg active:transform active:scale-95"
1373 <Icon name="delete" size={16} />
1376 onClick={() => toggleReturn(item.pid)}
1377 className={`px-3 py-1.5 ${item.isReturn ? 'bg-surface-2 text-text' : 'bg-warn text-surface'} hover:opacity-90 rounded-lg text-xs font-semibold transition-all shadow-md hover:shadow-lg active:transform active:scale-95`}
1378 title="Toggle return"
1380 <Icon name="undo" size={16} />
1383 onClick={() => openLineItemEdit(item)}
1384 className="px-3 py-1.5 bg-brand hover:opacity-90 text-surface rounded-lg text-xs font-semibold transition-all shadow-md hover:shadow-lg active:transform active:scale-95"
1385 title="Edit price/discount"
1387 <Icon name="edit" size={16} />
1395 {/* TOTAL Row - Enhanced Design */}
1396 <tr className="bg-gradient-to-r from-success/20 to-success/30 border-t-4 border-success">
1397 <td colSpan={5} className="px-4 py-4 text-right font-bold text-lg uppercase tracking-wide text-text">
1398 Sale Total (Inc GST):
1400 <td className="px-4 py-4 text-right font-extrabold text-3xl text-success">
1403 <td colSpan={2}></td>
1406 {/* Payment Options Row - Modern Button Design */}
1407 <tr className="bg-surface">
1408 <td colSpan={8} className="p-8">
1409 <div className="mb-4">
1410 <h3 className="text-xl font-bold text-text mb-4 flex items-center gap-2">
1411 <Icon name="credit_card" size={24} /> Payment Methods
1413 <div className="grid grid-cols-2 md:grid-cols-4 gap-3">
1414 {paymentMethods.map((method) => (
1417 onClick={() => processSale(method.name)}
1419 className="group px-6 py-4 bg-gradient-to-br from-brand to-brand2 hover:opacity-90 text-surface rounded-xl font-bold text-lg disabled:opacity-50 disabled:cursor-not-allowed transition-all shadow-xl hover:shadow-2xl hover:transform hover:scale-105 active:scale-95 flex flex-col items-center gap-2"
1421 <span className="text-3xl">{method.icon}</span>
1422 <span>{method.name}</span>
1428 <div className="border-t-2 border-border pt-6 mt-6">
1429 <h4 className="text-sm font-bold text-muted uppercase tracking-wide mb-3 flex items-center gap-2">
1430 <Icon name="settings" size={20} /> Special Actions
1432 <div className="flex gap-3 flex-wrap">
1434 className="px-5 py-2.5 bg-surface-2 hover:opacity-90 text-text rounded-lg font-semibold transition-all shadow-md hover:shadow-lg active:transform active:scale-95 flex items-center gap-2"
1436 <Icon name="inventory_2" size={20} /> Save to Picking
1439 className="px-5 py-2.5 bg-warn hover:opacity-90 text-surface rounded-lg font-semibold transition-all shadow-md hover:shadow-lg active:transform active:scale-95 flex items-center gap-2"
1441 <Icon name="warning" size={20} /> Mark Incomplete
1444 className="px-5 py-2.5 bg-brand2 hover:opacity-90 text-text rounded-lg font-semibold transition-all shadow-md hover:shadow-lg active:transform active:scale-95 flex items-center gap-2"
1446 <Icon name="description" size={20} /> Job Docket
1456 {/* Mobile Card View - Shows on screens smaller than lg */}
1457 <div className="lg:hidden space-y-3 border-2 border-slate-200 rounded-lg p-3 shadow-xl">
1458 {saleItems.map((item, index) => {
1459 const grossProfit = item.cost ? (item.total - (item.cost * item.qty)) : 0;
1460 const discountedPrice = item.discount ? item.price * (1 - item.discount / 100) : item.price;
1464 key={`${item.pid}-${index}`}
1465 className={`bg-surface border-2 rounded-lg shadow-md overflow-hidden ${item.isReturn ? 'border-danger bg-danger/10' : 'border-border'} mb-3 last:mb-0`}
1468 <div className="bg-gradient-to-r from-brand to-brand2 px-3 py-2 flex items-start justify-between">
1469 <div className="flex-1 pr-2">
1470 <div className="font-bold text-white text-sm leading-tight">{item.name}</div>
1471 {item.manufacturerCode && (
1472 <div className="text-xs text-white/80 mt-1 font-mono">{item.manufacturerCode}</div>
1475 <div className="text-right">
1476 <div className="text-xl font-bold text-white whitespace-nowrap">${item.total.toFixed(2)}</div>
1481 <div className="p-3 space-y-2">
1482 {/* Qty and Price */}
1483 <div className="grid grid-cols-2 gap-2 text-sm">
1485 <div className="text-xs text-muted font-semibold">Qty</div>
1489 onChange={(e) => updateQuantity(item.pid, Number(e.target.value))}
1490 className="w-full mt-1 px-2 py-1.5 border-2 border-slate-300 rounded text-center font-bold text-sm focus:ring-2 focus:ring-blue-500"
1494 <div className="text-xs text-slate-500 font-semibold">Each</div>
1495 <div className="mt-1 text-lg font-bold text-slate-700">${discountedPrice.toFixed(2)}</div>
1500 <div className="flex gap-2">
1502 onClick={() => openLineItemEdit(item)}
1503 className="flex-1 px-2 py-1.5 bg-brand hover:opacity-90 text-surface rounded font-semibold text-xs flex items-center justify-center gap-1"
1505 <Icon name="edit" size={14} /> Edit
1508 onClick={() => toggleReturn(item.pid)}
1509 className={`flex-1 px-2 py-1.5 ${item.isReturn ? 'bg-surface-2 text-text' : 'bg-warn text-surface'} hover:opacity-90 rounded font-semibold text-xs flex items-center justify-center gap-1`}
1511 <Icon name="undo" size={14} />
1514 onClick={() => removeFromSale(item.pid)}
1515 className="px-2 py-1.5 bg-danger hover:opacity-90 text-surface rounded font-semibold text-xs"
1517 <Icon name="delete" size={14} />
1525 {/* Mobile Total Banner */}
1526 <div className="bg-gradient-to-r from-success/20 to-success/30 border-4 border-success rounded-lg p-3 mt-3">
1527 <div className="flex items-center justify-between">
1528 <div className="text-sm sm:text-base font-bold uppercase text-text">Total:</div>
1529 <div className="text-2xl sm:text-3xl font-extrabold text-success">${total.toFixed(2)}</div>
1533 {/* Mobile Payment Buttons */}
1534 <div className="bg-surface border-2 border-border rounded-lg p-3 mt-3">
1535 <h3 className="text-base font-bold text-text mb-2 flex items-center gap-2">
1536 <Icon name="credit_card" size={20} /> Payment
1538 <div className="grid grid-cols-2 gap-2">
1539 {paymentMethods.map((method) => (
1542 onClick={() => processSale(method.name)}
1544 className="px-3 py-2 bg-gradient-to-br from-brand to-brand2 hover:opacity-90 text-surface rounded-lg font-bold text-xs disabled:opacity-50 transition-all shadow-lg active:scale-95 flex flex-col items-center gap-1"
1546 <span className="text-xl">{method.icon}</span>
1547 <span>{method.name}</span>
1555 <div className="p-6 sm:p-12 text-center border-2 border-dashed border-border rounded-2xl mb-6 bg-surface">
1556 <Icon name="shopping_cart" size={96} className="inline-block mb-3 sm:mb-4 text-muted" />
1557 <div className="text-xl sm:text-2xl font-bold text-text mb-2">No Items in Sale</div>
1558 <div className="text-sm sm:text-base text-muted">Search for products above to add them to the sale</div>
1563 <div className="mt-6 p-6 text-center bg-brand/10 border-2 border-brand rounded-xl">
1564 <div className="inline-block animate-spin rounded-full h-10 w-10 border-4 border-brand border-t-transparent mb-3"></div>
1565 <div className="text-brand font-bold text-lg">Processing sale...</div>
1572 {/* Customer Search Modal - Enhanced (Deprecated in favor of inline search) */}
1573 {showCustomerModal && (
1574 <div className="modal-overlay">
1575 <div className="bg-surface rounded-2xl shadow-2xl p-8 w-full max-w-3xl max-h-[80vh] overflow-y-auto border-2 border-border">
1576 <div className="flex justify-between items-center mb-6">
1577 <h2 className="text-3xl font-bold text-text">Select Customer</h2>
1579 onClick={() => { setShowCustomerModal(false); setCustomerSearch(''); }}
1580 className="text-slate-400 hover:text-slate-600 text-4xl font-bold transition-colors"
1588 value={customerSearch}
1589 onChange={(e) => setCustomerSearch(e.target.value)}
1590 placeholder="Search customers..."
1591 className="w-full px-4 py-2 border rounded-lg mb-4 focus:ring-2 focus:ring-blue-500 focus:outline-none"
1595 {customerLoading ? (
1596 <div className="text-center py-8">Loading customers...</div>
1598 <div className="space-y-2">
1602 c.name.toLowerCase().includes(customerSearch.toLowerCase()) ||
1603 (c.email && c.email.toLowerCase().includes(customerSearch.toLowerCase())) ||
1604 (c.phone && c.phone.includes(customerSearch))
1606 .map((customer) => (
1609 onClick={() => selectCustomer(customer)}
1610 className="p-4 border rounded-lg hover:bg-brand/10 cursor-pointer"
1612 <div className="font-semibold">{customer.name}</div>
1613 {customer.email && <div className="text-sm text-muted">{customer.email}</div>}
1614 {customer.phone && <div className="text-sm text-muted">{customer.phone}</div>}
1623 {/* Printer Search Modal - Enhanced */}
1624 {showPrinterSearch && (
1625 <div className="modal-overlay">
1626 <div className="bg-surface rounded-2xl shadow-2xl p-8 w-full max-w-4xl max-h-[80vh] overflow-y-auto border-2 border-border">
1627 <div className="flex justify-between items-center mb-6">
1628 <h2 className="text-3xl font-bold bg-gradient-to-r from-indigo-600 to-purple-600 bg-clip-text text-transparent flex items-center gap-3">
1629 <span>🖨️</span> {selectedPrinter ? `Cartridges for ${selectedPrinter.name}` : 'Printer Search'}
1633 setShowPrinterSearch(false);
1634 setPrinterSearchTerm("");
1635 setPrinterResults([]);
1636 setSelectedPrinter(null);
1637 setPrinterCartridges([]);
1639 className="text-slate-400 hover:text-slate-600 text-3xl font-bold transition-colors"
1645 {!selectedPrinter ? (
1649 value={printerSearchTerm}
1650 onChange={(e) => setPrinterSearchTerm(e.target.value)}
1651 placeholder="Search for printers (e.g., Canon, HP, Brother)..."
1652 className="w-full px-4 py-3 border-2 border-slate-300 rounded-xl mb-6 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 focus:outline-none transition-all text-base shadow-sm"
1656 {printerSearchTerm.length < 2 ? (
1657 <div className="text-center py-12 text-muted">
1658 <Icon name="print" size={96} className="inline-block mb-4" />
1659 <p className="text-lg">Enter at least 2 characters to search for printers</p>
1661 ) : printerResults.length === 0 ? (
1662 <div className="text-center py-12 text-muted">
1663 <Icon name="search" size={96} className="inline-block mb-4" />
1664 <p className="text-lg">No printers found matching "{printerSearchTerm}"</p>
1667 <div className="space-y-3">
1668 <p className="text-sm text-slate-600 font-semibold mb-4">
1669 Found {printerResults.length} printer{printerResults.length !== 1 ? 's' : ''} - Click to view compatible cartridges
1671 {printerResults.map((product) => (
1675 setSelectedPrinter(product);
1677 // Try to use stored relationships first
1678 const relationship = printerCartridgeRelationships.find(
1679 r => r.printerId === product.pid
1682 if (relationship && relationship.cartridges.length > 0) {
1683 // Use stored relationships
1684 console.log(`Using stored relationships for printer ${product.pid}:`, relationship.cartridges);
1685 const cartridges = allProducts.filter(p =>
1686 relationship.cartridges.includes(p.pid)
1688 console.log('Found cartridges from relationships:', cartridges.length);
1689 setPrinterCartridges(cartridges);
1691 // Fallback to heuristic matching if no relationships stored
1692 console.log('No stored relationships found, using heuristic matching');
1694 const printerText = `${product.name} ${product.description || ''} ${product.manufacturerCode || ''}`;
1695 console.log('Selected printer:', printerText);
1697 // Extract brand (common printer brands)
1698 const brandMatch = printerText.match(/\b(HP|Canon|Brother|Epson|Kyocera|Lexmark|Samsung|Dell|Xerox|Ricoh|Sharp|Konica|Minolta|Oki|Pantum|Fuji|Toshiba)\b/i);
1699 const brand = brandMatch ? brandMatch[0].toLowerCase() : '';
1701 // Extract printer model codes
1702 const modelPattern = /\b([A-Z]{1,4}[-\s]?[A-Z0-9]{3,8}[A-Z]{0,3})\b/gi;
1703 const models = printerText.match(modelPattern) || [];
1705 // Extract core model numbers
1706 const coreNumbers = printerText.match(/\b\d{4,5}\b/g) || [];
1708 console.log('Detected brand:', brand);
1709 console.log('Detected printer models:', models);
1710 console.log('Core model numbers:', coreNumbers);
1712 // Filter for cartridge products
1713 const normalize = (str: string) => str.replace(/[-\s]/g, '').toLowerCase();
1715 const cartridges = allProducts.filter(p => {
1716 const productText = `${p.name} ${p.description || ''} ${p.manufacturerCode || ''}`;
1717 const lowerText = productText.toLowerCase();
1719 // Must be a cartridge/toner/ink product
1720 const isCartridge = /\b(cartridge|toner|ink|drum)\b/i.test(productText);
1721 if (!isCartridge) return false;
1723 // Must have matching brand
1724 if (!brand || !lowerText.includes(brand)) return false;
1726 // Check if any printer model codes match (normalized)
1727 const normalizedProduct = normalize(productText);
1728 const hasExactModel = models.some(model => {
1729 const normalizedModel = normalize(model);
1730 return normalizedProduct.includes(normalizedModel);
1733 // Also check if core model numbers appear
1734 const hasCoreNumber = coreNumbers.some(num => productText.includes(num));
1736 return hasExactModel || hasCoreNumber;
1739 console.log('Found cartridges:', cartridges.length);
1740 setPrinterCartridges(cartridges.slice(0, 100));
1743 className="w-full flex justify-between items-center p-4 border-2 border-border rounded-xl hover:border-brand2 hover:bg-brand2/10 transition-all duration-200 text-left group shadow-sm hover:shadow-md"
1745 <div className="flex-1">
1746 <div className="font-bold text-text mb-1 group-hover:text-brand2 transition-colors">
1749 {product.description && (
1750 <div className="text-sm text-muted mb-2">{product.description}</div>
1752 <div className="flex gap-4 text-xs text-muted">
1753 {product.manufacturerCode && (
1754 <span className="flex items-center gap-1">
1755 <Icon name="local_offer" size={14} /> Model: {product.manufacturerCode}
1760 <div className="text-right ml-4">
1761 <div className="text-sm text-info font-semibold">
1774 setSelectedPrinter(null);
1775 setPrinterCartridges([]);
1777 className="mb-6 px-4 py-2 bg-slate-200 hover:bg-slate-300 text-slate-700 rounded-lg transition-all flex items-center gap-2 font-semibold"
1779 ← Back to Printer Search
1782 {printerCartridges.length === 0 ? (
1783 <div className="text-center py-12 text-muted">
1784 <Icon name="inventory_2" size={96} className="inline-block mb-4" />
1785 <p className="text-lg">No cartridges found for this printer</p>
1786 <p className="text-sm mt-2">Try searching for products directly or contact support</p>
1789 <div className="space-y-3">
1790 <p className="text-sm text-slate-600 font-semibold mb-4">
1791 Found {printerCartridges.length} compatible cartridge{printerCartridges.length !== 1 ? 's' : ''}
1793 {printerCartridges.map((product) => (
1798 setShowPrinterSearch(false);
1799 setPrinterSearchTerm("");
1800 setPrinterResults([]);
1801 setSelectedPrinter(null);
1802 setPrinterCartridges([]);
1804 className="w-full flex justify-between items-center p-4 border-2 border-border rounded-xl hover:border-success hover:bg-success/10 transition-all duration-200 text-left group shadow-sm hover:shadow-md"
1806 <div className="flex-1">
1807 <div className="font-bold text-text mb-1 group-hover:text-success transition-colors">
1810 {product.description && (
1811 <div className="text-sm text-muted mb-2">{product.description}</div>
1813 <div className="flex gap-4 text-xs text-muted">
1814 {product.barcode && (
1815 <span className="flex items-center gap-1">
1816 <Icon name="tag" size={14} /> {product.barcode}
1819 {product.manufacturerCode && (
1820 <span className="flex items-center gap-1">
1821 <Icon name="local_offer" size={14} /> {product.manufacturerCode}
1824 {product.stocklevel !== undefined && (
1825 <span className={`flex items-center gap-1 ${product.stocklevel > 0 ? 'text-success' : 'text-danger'}`}>
1826 <Icon name="inventory_2" size={14} /> Stock: {product.stocklevel}
1831 <div className="text-right ml-4">
1832 <div className="text-xl font-bold text-success">
1833 ${product.sellprice.toFixed(2)}
1835 <div className="text-xs text-slate-500 mt-1">
1849 {/* Line Item Edit Modal - Premium Design */}
1850 {showLineItemEdit && editingLineItem && (
1851 <div className="modal-overlay">
1852 <div className="bg-surface rounded-2xl shadow-2xl p-8 w-full max-w-md border-2 border-border transform transition-all">
1853 <h2 className="text-3xl font-bold bg-gradient-to-r from-brand to-brand2 bg-clip-text text-transparent mb-6 flex items-center gap-2">
1854 <Icon name="edit" size={32} /> Edit Line Item
1857 <div className="mb-6">
1858 <label className="block font-bold text-text mb-2 uppercase text-sm tracking-wide">Product</label>
1859 <div className="p-4 bg-surface-2 border-2 border-border rounded-xl text-text font-semibold">{editingLineItem.name}</div>
1862 <div className="mb-6">
1863 <label className="block font-bold text-text mb-2 uppercase text-sm tracking-wide flex items-center gap-2">
1864 <Icon name="attach_money" size={20} /> Price
1870 onChange={(e) => setTempPrice(parseFloat(e.target.value))}
1871 className="w-full px-4 py-3 border-2 border-border rounded-xl focus:ring-2 focus:ring-brand focus:border-brand focus:outline-none text-lg font-semibold transition-all"
1875 <div className="mb-6">
1876 <label className="block font-bold text-text mb-2 uppercase text-sm tracking-wide flex items-center gap-2">
1877 <Icon name="local_offer" size={20} /> Discount %
1882 value={tempDiscount}
1883 onChange={(e) => setTempDiscount(parseFloat(e.target.value))}
1884 className="w-full px-4 py-3 border-2 border-border rounded-xl focus:ring-2 focus:ring-brand focus:border-brand focus:outline-none text-lg font-semibold transition-all"
1888 {editingLineItem.cost && (
1889 <div className="mb-6 p-4 bg-gradient-to-br from-slate-50 to-blue-50 border-2 border-slate-200 rounded-xl">
1890 <div className="grid grid-cols-2 gap-4">
1892 <div className="text-xs font-bold text-slate-500 uppercase tracking-wide mb-1">Cost</div>
1893 <div className="text-lg font-bold text-slate-700">${editingLineItem.cost.toFixed(2)}</div>
1896 <div className="text-xs font-bold text-slate-500 uppercase tracking-wide mb-1">Margin $</div>
1897 <div className={`text-lg font-bold ${(() => {
1898 const discountedPrice = tempDiscount ? tempPrice * (1 - tempDiscount / 100) : tempPrice;
1899 const margin = (discountedPrice - editingLineItem.cost) * editingLineItem.qty;
1900 return margin >= 0 ? 'text-success' : 'text-danger';
1903 const discountedPrice = tempDiscount ? tempPrice * (1 - tempDiscount / 100) : tempPrice;
1904 const margin = (discountedPrice - editingLineItem.cost) * editingLineItem.qty;
1905 return margin.toFixed(2);
1910 <div className="text-xs font-bold text-slate-500 uppercase tracking-wide mb-1">Margin %</div>
1911 <div className={`text-lg font-bold ${(() => {
1912 const discountedPrice = tempDiscount ? tempPrice * (1 - tempDiscount / 100) : tempPrice;
1913 const marginPercent = discountedPrice > 0 ? ((discountedPrice - editingLineItem.cost) / discountedPrice) * 100 : 0;
1914 return marginPercent >= 0 ? 'text-success' : 'text-danger';
1917 const discountedPrice = tempDiscount ? tempPrice * (1 - tempDiscount / 100) : tempPrice;
1918 const marginPercent = discountedPrice > 0 ? ((discountedPrice - editingLineItem.cost) / discountedPrice) * 100 : 0;
1919 return marginPercent.toFixed(1);
1924 <div className="text-xs font-bold text-slate-500 uppercase tracking-wide mb-1">Total Sale</div>
1925 <div className="text-lg font-bold text-brand">
1927 const discountedPrice = tempDiscount ? tempPrice * (1 - tempDiscount / 100) : tempPrice;
1928 return (discountedPrice * editingLineItem.qty).toFixed(2);
1936 <div className="flex gap-3">
1938 onClick={saveLineItemEdit}
1939 className="flex-1 px-6 py-3 bg-gradient-to-r from-brand to-brand2 text-surface rounded-xl hover:opacity-90 font-bold text-lg shadow-xl hover:shadow-2xl transition-all active:transform active:scale-95"
1941 <Icon name="check" size={20} className="inline-block mr-2" /> Save Changes
1944 onClick={() => setShowLineItemEdit(false)}
1945 className="flex-1 px-6 py-3 bg-surface-2 rounded-xl hover:opacity-90 font-bold text-lg text-text shadow-md hover:shadow-lg transition-all active:transform active:scale-95"
1947 <Icon name="close" size={20} className="inline-block mr-2" /> Cancel
1954 {/* Parked Sales Modal */}
1955 {showParkedSales && (
1956 <div className="modal-overlay-light">
1957 <div className="bg-surface rounded-lg shadow-2xl p-6 w-full max-w-3xl max-h-[80vh] overflow-y-auto border border-border">
1958 <div className="flex justify-between items-center mb-4">
1959 <h2 className="text-2xl font-bold">Parked Sales</h2>
1961 onClick={() => setShowParkedSales(false)}
1962 className="text-muted hover:text-text text-2xl"
1968 {parkedSales.length === 0 ? (
1969 <div className="text-center py-8 text-muted">No parked sales</div>
1971 <div className="space-y-3">
1972 {parkedSales.map((sale) => (
1973 <div key={sale.id} className="border rounded-lg p-4 hover:bg-surface-2">
1974 <div className="flex justify-between items-start mb-2">
1976 <div className="font-semibold">Sale #{sale.id}</div>
1977 {sale.customer && <div className="text-sm text-muted">{sale.customer.name}</div>}
1978 <div className="text-sm text-muted">{new Date(sale.timestamp).toLocaleString()}</div>
1979 {sale.orderNumber && <div className="text-sm text-muted">Order: {sale.orderNumber}</div>}
1981 <div className="text-right">
1982 <div className="text-xl font-bold text-brand">${sale.total.toFixed(2)}</div>
1983 <div className="text-sm text-muted">{sale.items.length} items</div>
1987 onClick={() => { unparkSale(sale); setShowParkedSales(false); }}
1988 className="w-full px-4 py-2 bg-success text-surface rounded hover:opacity-90"
2000 {/* Past Sales Modal */}
2002 <div className="modal-overlay-light">
2003 <div className="bg-surface rounded-lg shadow-2xl p-6 w-full max-w-4xl max-h-[80vh] overflow-y-auto border border-border">
2004 <div className="flex justify-between items-center mb-4">
2005 <h2 className="text-2xl font-bold">Past Sales</h2>
2007 onClick={() => setShowPastSales(false)}
2008 className="text-muted hover:text-text text-2xl"
2014 {pastSales.length === 0 ? (
2015 <div className="text-center py-8 text-muted">No past sales found</div>
2017 <div className="overflow-x-auto">
2018 <table className="w-full text-sm">
2019 <thead className="bg-surface-2 border-b-2">
2021 <th className="text-left p-2">Date</th>
2022 <th className="text-left p-2">Order #</th>
2023 <th className="text-left p-2">Customer</th>
2024 <th className="text-right p-2">Total</th>
2025 <th className="text-left p-2">Payment</th>
2029 {pastSales.map((sale: any) => (
2030 <tr key={sale.SaleId || sale.ExternalId} className="border-b hover:bg-surface-2">
2031 <td className="p-2">{new Date(sale.Date || sale.CreateDate).toLocaleString()}</td>
2032 <td className="p-2">{sale.OrderNo || '-'}</td>
2033 <td className="p-2">{sale.CustomerName || 'Cash Sale'}</td>
2034 <td className="p-2 text-right font-semibold">${parseFloat(sale.Total || 0).toFixed(2)}</td>
2035 <td className="p-2">{sale.PaymentMethod || 'N/A'}</td>
2046 {/* Receipt Options Modal */}
2047 {showReceiptOptions && (
2048 <div className="modal-overlay-light">
2049 <div className="bg-surface rounded-lg shadow-2xl p-6 w-full max-w-md border border-border">
2050 <h2 className="text-2xl font-bold mb-4">Receipt Options</h2>
2052 <div className="mb-4">
2053 <label className="flex items-center space-x-2 mb-3">
2056 checked={printReceipt}
2057 onChange={(e) => setPrintReceipt(e.target.checked)}
2060 <span className="font-semibold">Print Receipt</span>
2063 <label className="flex items-center space-x-2">
2066 checked={emailReceipt}
2067 onChange={(e) => setEmailReceipt(e.target.checked)}
2070 <span className="font-semibold">Email Receipt</span>
2075 <div className="mb-4">
2076 <label className="block font-semibold mb-2">Email Address</label>
2079 value={receiptEmail}
2080 onChange={(e) => setReceiptEmail(e.target.value)}
2081 className="w-full px-4 py-2 border rounded focus:ring-2 focus:ring-blue-500 focus:outline-none"
2082 placeholder="customer@example.com"
2087 <div className="flex gap-2">
2089 onClick={() => selectedPaymentMethod && processSale(selectedPaymentMethod)}
2090 disabled={emailReceipt && !receiptEmail}
2091 className="flex-1 px-4 py-2 bg-success text-surface rounded hover:opacity-90 disabled:opacity-50"
2096 onClick={() => { setShowReceiptOptions(false); setSelectedPaymentMethod(''); }}
2097 className="flex-1 px-4 py-2 bg-surface-2 rounded hover:opacity-90"
2106 {/* End of Day Modal */}
2108 <div className="modal-overlay-light">
2109 <div className="bg-surface rounded-lg shadow-2xl p-6 w-full max-w-2xl border border-border">
2110 <div className="flex justify-between items-center mb-4">
2111 <h2 className="text-2xl font-bold">End of Day</h2>
2113 onClick={() => setShowEndOfDay(false)}
2114 className="text-muted hover:text-text text-2xl"
2120 <div className="space-y-4">
2121 <div className="bg-surface-2 p-4 rounded">
2122 <div className="font-semibold mb-2">Daily Summary</div>
2123 <div className="text-sm text-muted">End of day reconciliation feature coming soon...</div>
2127 onClick={() => setShowEndOfDay(false)}
2128 className="w-full px-4 py-2 bg-brand text-surface rounded hover:opacity-90"
2137 {/* Billing Address Edit Modal */}
2138 {showBillingEdit && selectedCustomer && (
2139 <div className="modal-overlay-light">
2140 <div className="bg-surface rounded-lg shadow-2xl p-6 w-full max-w-lg">
2141 <div className="flex justify-between items-center mb-4">
2142 <h2 className="text-2xl font-bold">Edit Billing Address</h2>
2144 onClick={() => setShowBillingEdit(false)}
2145 className="text-muted hover:text-text text-2xl"
2151 <div className="space-y-3">
2153 <label className="block text-sm font-semibold mb-1">Address Line 1</label>
2156 value={editingAddress?.billingAddress1 || ''}
2157 onChange={(e) => setEditingAddress({...editingAddress!, billingAddress1: e.target.value})}
2158 className="w-full px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500 focus:outline-none"
2162 <label className="block text-sm font-semibold mb-1">Address Line 2</label>
2165 value={editingAddress?.billingAddress2 || ''}
2166 onChange={(e) => setEditingAddress({...editingAddress!, billingAddress2: e.target.value})}
2167 className="w-full px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500 focus:outline-none"
2170 <div className="grid grid-cols-2 gap-3">
2172 <label className="block text-sm font-semibold mb-1">City</label>
2175 value={editingAddress?.billingCity || ''}
2176 onChange={(e) => setEditingAddress({...editingAddress!, billingCity: e.target.value})}
2177 className="w-full px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500 focus:outline-none"
2181 <label className="block text-sm font-semibold mb-1">State</label>
2184 value={editingAddress?.billingState || ''}
2185 onChange={(e) => setEditingAddress({...editingAddress!, billingState: e.target.value})}
2186 className="w-full px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500 focus:outline-none"
2190 <div className="grid grid-cols-2 gap-3">
2192 <label className="block text-sm font-semibold mb-1">Postcode</label>
2195 value={editingAddress?.billingPostcode || ''}
2196 onChange={(e) => setEditingAddress({...editingAddress!, billingPostcode: e.target.value})}
2197 className="w-full px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500 focus:outline-none"
2201 <label className="block text-sm font-semibold mb-1">Country</label>
2204 value={editingAddress?.billingCountry || ''}
2205 onChange={(e) => setEditingAddress({...editingAddress!, billingCountry: e.target.value})}
2206 className="w-full px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500 focus:outline-none"
2210 <div className="flex gap-3 pt-4">
2212 onClick={async () => {
2213 if (editingAddress) {
2215 // Update customer via API - note: API may not support all address fields yet
2216 const result = await apiClient.updateCustomer(editingAddress.cid, {
2217 name: editingAddress.name,
2218 company: editingAddress.company,
2219 phone: editingAddress.phone,
2220 mobile: editingAddress.mobile,
2221 email: editingAddress.email
2224 if (result.success) {
2225 setSelectedCustomer(editingAddress);
2226 setShowBillingEdit(false);
2227 alert('✅ Billing address updated successfully!');
2229 alert('❌ Failed to update billing address: ' + (result.error || 'Unknown error'));
2232 console.error('Failed to update billing address:', error);
2233 alert('❌ Failed to update billing address. Please try again.');
2237 className="flex-1 px-4 py-2 bg-brand text-surface rounded hover:opacity-90 font-semibold"
2242 onClick={() => setShowBillingEdit(false)}
2243 className="flex-1 px-4 py-2 bg-surface-2 text-text rounded hover:opacity-90 font-semibold"
2253 {/* Delivery Address Edit Modal */}
2254 {showShippingEdit && selectedCustomer && (
2255 <div className="modal-overlay-light">
2256 <div className="bg-surface rounded-lg shadow-2xl p-6 w-full max-w-lg">
2257 <div className="flex justify-between items-center mb-4">
2258 <h2 className="text-2xl font-bold">Select/Edit Delivery Address</h2>
2260 onClick={() => setShowShippingEdit(false)}
2261 className="text-muted hover:text-text text-2xl"
2267 <div className="mb-4">
2270 if (editingAddress && selectedCustomer) {
2273 deliveryAddress1: selectedCustomer.billingAddress1,
2274 deliveryAddress2: selectedCustomer.billingAddress2,
2275 deliveryCity: selectedCustomer.billingCity,
2276 deliveryState: selectedCustomer.billingState,
2277 deliveryPostcode: selectedCustomer.billingPostcode,
2278 deliveryCountry: selectedCustomer.billingCountry
2282 className="w-full px-4 py-2 bg-surface-2 hover:bg-surface-2/80 rounded font-semibold"
2284 Copy from Billing Address
2288 <div className="space-y-3">
2290 <label className="block text-sm font-semibold mb-1">Address Line 1</label>
2293 value={editingAddress?.deliveryAddress1 || ''}
2294 onChange={(e) => setEditingAddress({...editingAddress!, deliveryAddress1: e.target.value})}
2295 className="w-full px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500 focus:outline-none"
2299 <label className="block text-sm font-semibold mb-1">Address Line 2</label>
2302 value={editingAddress?.deliveryAddress2 || ''}
2303 onChange={(e) => setEditingAddress({...editingAddress!, deliveryAddress2: e.target.value})}
2304 className="w-full px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500 focus:outline-none"
2307 <div className="grid grid-cols-2 gap-3">
2309 <label className="block text-sm font-semibold mb-1">City</label>
2312 value={editingAddress?.deliveryCity || ''}
2313 onChange={(e) => setEditingAddress({...editingAddress!, deliveryCity: e.target.value})}
2314 className="w-full px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500 focus:outline-none"
2318 <label className="block text-sm font-semibold mb-1">State</label>
2321 value={editingAddress?.deliveryState || ''}
2322 onChange={(e) => setEditingAddress({...editingAddress!, deliveryState: e.target.value})}
2323 className="w-full px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500 focus:outline-none"
2327 <div className="grid grid-cols-2 gap-3">
2329 <label className="block text-sm font-semibold mb-1">Postcode</label>
2332 value={editingAddress?.deliveryPostcode || ''}
2333 onChange={(e) => setEditingAddress({...editingAddress!, deliveryPostcode: e.target.value})}
2334 className="w-full px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500 focus:outline-none"
2338 <label className="block text-sm font-semibold mb-1">Country</label>
2341 value={editingAddress?.deliveryCountry || ''}
2342 onChange={(e) => setEditingAddress({...editingAddress!, deliveryCountry: e.target.value})}
2343 className="w-full px-3 py-2 border rounded focus:ring-2 focus:ring-blue-500 focus:outline-none"
2347 <div className="flex gap-3 pt-4">
2349 onClick={async () => {
2350 if (editingAddress) {
2352 // Update customer via API - note: API may not support all address fields yet
2353 const result = await apiClient.updateCustomer(editingAddress.cid, {
2354 name: editingAddress.name,
2355 company: editingAddress.company,
2356 phone: editingAddress.phone,
2357 mobile: editingAddress.mobile,
2358 email: editingAddress.email
2361 if (result.success) {
2362 setSelectedCustomer(editingAddress);
2363 setShowShippingEdit(false);
2364 alert('✅ Delivery address updated successfully!');
2366 alert('❌ Failed to update delivery address: ' + (result.error || 'Unknown error'));
2369 console.error('Failed to update delivery address:', error);
2370 alert('❌ Failed to update delivery address. Please try again.');
2374 className="flex-1 px-4 py-2 bg-brand text-surface rounded hover:opacity-90 font-semibold"
2379 onClick={() => setShowShippingEdit(false)}
2380 className="flex-1 px-4 py-2 bg-surface-2 text-text rounded hover:opacity-90 font-semibold"
2390 {/* Serial Number Entry Modal */}
2391 {showSerialModal && pendingSerialProduct && (
2392 <div className="modal-overlay">
2393 <div className="bg-surface rounded-2xl shadow-2xl p-8 w-full max-w-md border-2 border-border">
2394 <h2 className="text-3xl font-bold bg-gradient-to-r from-brand to-brand2 bg-clip-text text-transparent mb-6 flex items-center gap-2">
2395 <Icon name="tag" size={32} /> Enter Serial Number
2398 <div className="mb-6">
2399 <label className="block font-bold text-text mb-2 uppercase text-sm tracking-wide">Product</label>
2400 <div className="p-4 bg-surface-2 border-2 border-border rounded-xl text-text font-semibold">{pendingSerialProduct.name}</div>
2403 <div className="mb-6">
2404 <label className="block font-bold text-text mb-2 uppercase text-sm tracking-wide flex items-center gap-2">
2405 <Icon name="tag" size={20} /> Serial Number
2409 value={serialNumber}
2410 onChange={(e) => setSerialNumber(e.target.value)}
2411 placeholder="Enter serial number..."
2412 className="w-full px-4 py-3 border-2 border-border rounded-xl focus:ring-2 focus:ring-brand focus:border-brand focus:outline-none text-lg font-mono font-semibold transition-all"
2415 if (e.key === 'Enter' && serialNumber.trim()) {
2416 addToSaleWithSerial(serialNumber.trim());
2417 } else if (e.key === 'Escape') {
2418 setShowSerialModal(false);
2419 setPendingSerialProduct(null);
2420 setSerialNumber('');
2424 <p className="mt-2 text-sm text-slate-500">Press Enter to add, Esc to cancel</p>
2427 <div className="flex gap-3">
2430 if (serialNumber.trim()) {
2431 addToSaleWithSerial(serialNumber.trim());
2434 disabled={!serialNumber.trim()}
2435 className="flex-1 px-6 py-3 bg-gradient-to-r from-brand to-brand2 text-surface rounded-xl hover:opacity-90 font-bold text-lg shadow-xl hover:shadow-2xl transition-all active:transform active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed"
2437 <Icon name="check" size={20} className="inline-block mr-2" /> Add to Sale
2441 setShowSerialModal(false);
2442 setPendingSerialProduct(null);
2443 setSerialNumber('');
2445 className="flex-1 px-6 py-3 bg-surface-2 rounded-xl hover:opacity-90 font-bold text-lg text-text shadow-md hover:shadow-lg transition-all active:transform active:scale-95"
2447 <Icon name="close" size={20} className="inline-block mr-2" /> Cancel