EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
page.tsx
Go to the documentation of this file.
1"use client";
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';
8
9interface Product {
10 pid: number;
11 name: string;
12 barcode?: string;
13 sellprice: number;
14 description?: string;
15 stocklevel?: number;
16 cost?: number;
17 manufacturerCode?: string;
18 requiresSerial?: boolean;
19 department?: string;
20 category?: string;
21}
22
23interface SaleItem {
24 pid: number;
25 name: string;
26 qty: number;
27 price: number;
28 discount: number;
29 total: number;
30 cost?: number;
31 isReturn?: boolean;
32 manufacturerCode?: string;
33 serialNumber?: string;
34}
35
36interface PaymentMethod {
37 name: string;
38 code: string;
39 icon: string;
40}
41
42interface Customer {
43 cid: number;
44 name: string;
45 email?: string;
46 phone?: string;
47 mobile?: string;
48 accountBalance?: number;
49 address1?: string;
50 address2?: string;
51 city?: string;
52 state?: string;
53 postcode?: string;
54 country?: string;
55 billingAddress1?: string;
56 billingAddress2?: string;
57 billingCity?: 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;
67}
68
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);
86
87 // Default walk-in customer
88 const defaultWalkInCustomer: Customer = {
89 cid: 0,
90 name: "Cash Sale",
91 email: "",
92 phone: "",
93 mobile: "",
94 accountBalance: 0,
95 address1: "",
96 address2: "",
97 city: "",
98 state: "",
99 postcode: "",
100 country: "",
101 deliveryAddress1: "",
102 deliveryAddress2: "",
103 deliveryCity: "",
104 deliveryState: "",
105 deliveryPostcode: "",
106 deliveryCountry: ""
107 };
108
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);
148
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: "💰" }
157 ];
158
159 useEffect(() => {
160 loadPrinterCartridgeRelationships();
161 }, []);
162
163 // Debounced search effect
164 useEffect(() => {
165 if (searchTerm.length >= 2) {
166 const timeoutId = setTimeout(() => {
167 loadProducts(searchTerm);
168 }, 300);
169 return () => clearTimeout(timeoutId);
170 }
171 }, [searchTerm]);
172
173 // Keyboard shortcut: F2 to focus barcode input
174 useEffect(() => {
175 const handleKeyDown = (e: KeyboardEvent) => {
176 if (e.key === 'F2') {
177 e.preventDefault();
178 barcodeInputRef.current?.focus();
179 }
180 };
181
182 window.addEventListener('keydown', handleKeyDown);
183 return () => window.removeEventListener('keydown', handleKeyDown);
184 }, []);
185
186 useEffect(() => {
187 const newTotal = saleItems.reduce((sum, item) => sum + item.total, 0);
188 setTotal(newTotal);
189 }, [saleItems]);
190
191 // Initialize customer display broadcast channel
192 useEffect(() => {
193 if (typeof BroadcastChannel !== 'undefined') {
194 displayChannel.current = new BroadcastChannel('pos-display');
195 }
196 return () => {
197 displayChannel.current?.close();
198 };
199 }, []);
200
201 // Broadcast cart updates to customer display
202 useEffect(() => {
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
207
208 displayChannel.current.postMessage({
209 type: 'cart-update',
210 cart: saleItems.map(item => ({
211 id: item.pid,
212 name: item.name,
213 price: item.price,
214 quantity: item.qty
215 })),
216 total,
217 subtotal,
218 tax,
219 customer: selectedCustomer?.cid !== 0 ? { name: selectedCustomer?.name } : null
220 });
221 } else {
222 displayChannel.current.postMessage({ type: 'clear' });
223 }
224 }
225 }, [saleItems, total, selectedCustomer]);
226
227 // Client-side product filtering for instant results
228 useEffect(() => {
229 if (searchTerm.length === 0) {
230 setProducts([]);
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);
235
236 console.log(`Searching for keywords: ${keywords.join(', ')} in ${allProducts.length} products`);
237
238 const filtered = allProducts.filter(p => {
239 // Combine all searchable fields
240 const searchableText = [
241 p.name,
242 p.description || '',
243 p.barcode || '',
244 p.manufacturerCode || ''
245 ].join(' ').toLowerCase();
246
247 const normalizedText = normalizeString(searchableText);
248
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);
253 });
254
255 return matches;
256 });
257
258 console.log(`Found ${filtered.length} matches`);
259 setProducts(filtered.slice(0, 50)); // Limit to 50 results for performance
260 }
261 }, [searchTerm, allProducts]);
262
263 // Auto-load customers when user starts typing (lazy loading)
264 useEffect(() => {
265 if (customerSearch.length >= 2 && allCustomers.length === 0 && !customerLoading) {
266 console.log('Customer search triggered - loading all customers for filtering');
267 loadCustomers();
268 }
269 }, [customerSearch]);
270
271 // Client-side instant customer filtering
272 useEffect(() => {
273 if (customerSearch.length === 0) {
274 setCustomers([]);
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 || ''}`
280 );
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));
284 });
285 setCustomers(filtered.slice(0, 50)); // Limit to 50 results for performance
286 }
287 }, [customerSearch, allCustomers]);
288
289 // Client-side instant printer filtering - only show actual printers
290 useEffect(() => {
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);
301
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);
307
308 const isPrinter = isPrinterByDepartment || isPrinterByCategory || isPrinterByKeyword;
309 if (!isPrinter) return false;
310
311 // Then check if it matches the search term
312 const searchableText = normalize(
313 `${p.name} ${p.description || ''} ${p.barcode || ''} ${p.manufacturerCode || ''}`
314 );
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));
318 });
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 })));
322 }
323 setPrinterResults(filtered.slice(0, 50));
324 }
325 }, [printerSearchTerm, allProducts]);
326
327 // Keyboard shortcuts
328 useEffect(() => {
329 const handleKeyPress = (e: KeyboardEvent) => {
330 // F2 - Select Customer
331 if (e.key === 'F2') {
332 e.preventDefault();
333 setShowCustomerModal(true);
334 loadCustomers();
335 }
336 // F8 - Void Sale
337 else if (e.key === 'F8') {
338 e.preventDefault();
339 if (saleItems.length > 0) voidSale();
340 }
341 // F9 - Park Sale
342 else if (e.key === 'F9') {
343 e.preventDefault();
344 if (saleItems.length > 0) parkSale();
345 }
346 // F10 - Unpark Sale
347 else if (e.key === 'F10') {
348 e.preventDefault();
349 if (parkedSales.length > 0) setShowParkedSales(true);
350 }
351 };
352
353 window.addEventListener('keydown', handleKeyPress);
354 return () => window.removeEventListener('keydown', handleKeyPress);
355 }, [saleItems, parkedSales]);
356
357 async function loadPrinterCartridgeRelationships() {
358 try {
359 const response = await fetch('/api/v1/printers-cartridges');
360 const result = await response.json();
361
362 if (result.success && result.data) {
363 setPrinterCartridgeRelationships(result.data);
364 console.log('Loaded printer-cartridge relationships:', result.data.length);
365 }
366 } catch (error) {
367 console.error('Failed to load printer-cartridge relationships:', error);
368 }
369 }
370
371 async function loadProducts(searchQuery?: string, barcodeQuery?: string) {
372 try {
373 setLoading(true);
374
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;
379
380 type ApiResult = {
381 success: boolean;
382 data?: any;
383 error?: string;
384 };
385 const result: ApiResult = await apiClient.getProducts(params);
386
387 console.log('API result:', result);
388
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)
392 ? result.data
393 : result.data?.value || result.data?.data?.Product || result.data?.Product || result.data;
394
395 console.log('Products data:', productsData);
396
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
401 .map((p: any) => ({
402 pid: p.Pid || p.pid,
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
413 }));
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');
418 setAllProducts([]);
419 } else {
420 console.warn('Unexpected products data format (not an array):', typeof productsData, productsData);
421 setAllProducts([]);
422 }
423 } else {
424 console.error("API call failed or no data:", result);
425 }
426 } catch (error) {
427 console.error("Failed to load products:", error);
428 } finally {
429 setLoading(false);
430 }
431 }
432
433 function addToSale(product: Product, qty = quantity) {
434 if (!product || !product.pid) {
435 console.error('Invalid product', product);
436 return;
437 }
438
439 // Check if product requires serial number
440 if (product.requiresSerial && qty === 1) {
441 setPendingSerialProduct(product);
442 setShowSerialModal(true);
443 return;
444 }
445
446 const existingItem = saleItems.find(item => item.pid === product.pid);
447
448 if (existingItem) {
449 setSaleItems(items =>
450 items.map(item =>
451 item.pid === product.pid
452 ? { ...item, qty: item.qty + qty, total: (item.qty + qty) * item.price }
453 : item
454 )
455 );
456 } else {
457 const newItem: SaleItem = {
458 pid: product.pid,
459 name: product.name,
460 qty,
461 price: product.sellprice,
462 total: qty * product.sellprice,
463 cost: product.cost,
464 manufacturerCode: product.manufacturerCode,
465 discount: 0
466 };
467 setSaleItems(items => [...items, newItem]);
468 }
469
470 setQuantity(1);
471 setBarcode('');
472 setSelectedProduct(null);
473 }
474
475 function addToSaleWithSerial(serial: string) {
476 if (!pendingSerialProduct) return;
477
478 const newItem: SaleItem = {
479 pid: pendingSerialProduct.pid,
480 name: pendingSerialProduct.name,
481 qty: 1,
482 price: pendingSerialProduct.sellprice,
483 total: pendingSerialProduct.sellprice,
484 serialNumber: serial,
485 cost: pendingSerialProduct.cost,
486 manufacturerCode: pendingSerialProduct.manufacturerCode,
487 discount: 0
488 };
489
490 setSaleItems(items => [...items, newItem]);
491 setQuantity(1);
492 setBarcode('');
493 setSelectedProduct(null);
494 setShowSerialModal(false);
495 setPendingSerialProduct(null);
496 setSerialNumber('');
497 }
498
499 function updateSerialNumber(pid: number, serial: string) {
500 setSaleItems(items =>
501 items.map(item =>
502 item.pid === pid
503 ? { ...item, serialNumber: serial }
504 : item
505 )
506 );
507 setEditingSerialPid(null);
508 setTempSerialNumber('');
509 }
510
511 function startEditingSerial(pid: number, currentSerial?: string) {
512 setEditingSerialPid(pid);
513 setTempSerialNumber(currentSerial || '');
514 }
515
516 function removeFromSale(pid: number) {
517 setSaleItems(items => items.filter(item => item.pid !== pid));
518 }
519
520 function updateQuantity(pid: number, newQty: number) {
521 if (newQty <= 0) {
522 removeFromSale(pid);
523 return;
524 }
525
526 setSaleItems(items =>
527 items.map(item =>
528 item.pid === pid
529 ? { ...item, qty: newQty, total: (newQty * item.price) - ((newQty * item.price * item.discount) / 100) }
530 : item
531 )
532 );
533 }
534
535 async function loadCustomers(searchQuery?: string) {
536 try {
537 setCustomerLoading(true);
538
539 const params: any = { limit: 10000, source: 'openapi' }; // Load all customers for client-side filtering
540
541 type ApiResult = {
542 success: boolean;
543 data?: any;
544 error?: string;
545 };
546 const result: ApiResult = await apiClient.getCustomers(params);
547
548 console.log('Customer API response:', result);
549
550 if (result.success && result.data) {
551 const customersData = Array.isArray(result.data)
552 ? result.data
553 : result.data?.data?.Customer || result.data;
554
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)
559 .map((c: any) => ({
560 cid: c.Cid || c.cid,
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
578 }));
579 setAllCustomers(mappedCustomers); // Store all customers
580 // Don't set customers here - let the useEffect handle filtering based on search
581 } else {
582 console.warn('Customer API returned empty or invalid data:', customersData);
583 }
584 } else {
585 console.warn('Customer API call unsuccessful or no data:', result);
586 }
587 } catch (error) {
588 console.error('Failed to load customers:', error);
589 } finally {
590 setCustomerLoading(false);
591 }
592 }
593
594 function selectCustomer(customer: Customer | null) {
595 setSelectedCustomer(customer);
596 setShowCustomerModal(false);
597 setCustomerSearch("");
598 }
599
600 function openLineItemEdit(item: SaleItem) {
601 setEditingLineItem(item);
602 setTempPrice(item.price);
603 setTempDiscount(item.discount || 0);
604 setShowLineItemEdit(true);
605 }
606
607 function saveLineItemEdit() {
608 if (!editingLineItem) return;
609
610 setSaleItems(items =>
611 items.map(item =>
612 item.pid === editingLineItem.pid
613 ? {
614 ...item,
615 price: tempPrice,
616 discount: tempDiscount,
617 total: (item.qty * tempPrice) - ((item.qty * tempPrice * tempDiscount) / 100)
618 }
619 : item
620 )
621 );
622 setShowLineItemEdit(false);
623 setEditingLineItem(null);
624 }
625
626 function toggleReturn(pid: number) {
627 setSaleItems(items =>
628 items.map(item =>
629 item.pid === pid
630 ? { ...item, isReturn: !item.isReturn }
631 : item
632 )
633 );
634 }
635
636 function parkSale() {
637 if (saleItems.length === 0) {
638 alert('No items to park');
639 return;
640 }
641
642 const parkedSale = {
643 id: `PARK_${Date.now()}`,
644 items: saleItems,
645 customer: selectedCustomer,
646 total,
647 orderNumber,
648 comments: saleComments,
649 internalComments,
650 timestamp: new Date().toISOString()
651 };
652
653 setParkedSales([...parkedSales, parkedSale]);
654 clearSale();
655 alert('Sale parked successfully');
656 }
657
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');
667 }
668
669 function voidSale() {
670 if (saleItems.length === 0) {
671 alert('No sale to void');
672 return;
673 }
674
675 if (confirm('Are you sure you want to VOID this sale? This cannot be undone.')) {
676 clearSale();
677 alert('Sale voided');
678 }
679 }
680
681 async function loadPastSales() {
682 try {
683 setLoading(true);
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);
688 }
689 } catch (error) {
690 console.error('Failed to load past sales:', error);
691 } finally {
692 setLoading(false);
693 }
694 }
695
696 function clearSale() {
697 setSaleItems([]);
698 setTotal(0);
699 setShowPayment(false);
700 setSelectedCustomer(defaultWalkInCustomer);
701 setCustomerSearch("");
702 setSaleComments("");
703 setInternalComments("");
704 setOrderNumber("");
705 }
706
707 async function processSale(paymentMethod: string) {
708 if (saleItems.length === 0) {
709 alert('No items in sale');
710 return;
711 }
712
713 // Show receipt options first
714 if (!showReceiptOptions) {
715 setReceiptEmail(selectedCustomer?.email || '');
716 setSelectedPaymentMethod(paymentMethod);
717 setShowReceiptOptions(true);
718 return;
719 }
720
721 try {
722 setLoading(true);
723
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',
734 StoreId: store?.id,
735 StoreName: store?.name,
736 ReceiptEmail: emailReceipt ? receiptEmail : undefined,
737 PrintReceipt: printReceipt,
738 LINE: saleItems.map(item => ({
739 Pid: item.pid,
740 Qty: item.qty,
741 TotalPrice: item.total
742 })),
743 PAYM: [{
744 Type: paymentMethod.toLowerCase(),
745 Amount: total
746 }]
747 };
748
749 // Add customer if selected
750 if (selectedCustomer) {
751 saleData.Cid = selectedCustomer.cid;
752 saleData.CustomerName = selectedCustomer.name;
753 }
754
755 // Use API helper to process sale
756 const result = await apiClient.createSale(saleData);
757
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);
763 clearSale();
764 } else {
765 console.error("Sale processing failed:", result.error);
766 alert(`❌ Sale processing failed: ${result.error || 'Please try again.'}`);
767 }
768 } catch (error) {
769 console.error("Sale processing failed:", error);
770 alert(`❌ Sale processing failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
771 } finally {
772 setLoading(false);
773 }
774 }
775
776 return (
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">
783 <button
784 onClick={() => {
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);
790 }}
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"
792 >
793 <Icon name="assessment" size={16} className="sm:text-base" /> <span className="hidden sm:inline">Past Sales</span><span className="sm:hidden">Past</span>
794 </button>
795 <button
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"
798 >
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>
800 </button>
801 <button
802 onClick={parkSale}
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"
805 >
806 <Icon name="pause" size={16} className="sm:text-base" /> <span className="hidden sm:inline">Park</span>
807 </button>
808 <button
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"
812 >
813 <Icon name="play_arrow" size={16} className="sm:text-base" /> <span className="hidden sm:inline">Unpark</span>
814 </button>
815 <button
816 onClick={voidSale}
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"
819 >
820 <Icon name="close" size={16} className="sm:text-base" /> <span className="hidden sm:inline">Void</span>
821 </button>
822 <button
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"
824 >
825 <Icon name="smartphone" size={16} className="sm:text-base" /> <span className="hidden lg:inline">Manual Barcode</span><span className="lg:hidden">Barcode</span>
826 </button>
827 <button
828 onClick={() => {
829 window.open(
830 '/pages/sales/display',
831 'customer-display',
832 'width=1024,height=768'
833 );
834 }}
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"
836 >
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>
838 </button>
839 </div>
840 </div>
841 </div>
842
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>
847 </h1>
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>
851 </div>
852 </div>
853
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>
861 </h2>
862 {selectedCustomer && (
863 <button
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"
866 >
867 <Icon name="sync" size={16} /> Change Customer
868 </button>
869 )}
870 </div>
871
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" />
879 </div>
880 <input
881 type="text"
882 value={customerSearch}
883 onChange={(e) => {
884 setCustomerSearch(e.target.value);
885 }}
886 onKeyDown={(e) => {
887 if (e.key === 'Escape') {
888 setCustomerSearch('');
889 setCustomers([]);
890 }
891 }}
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"
894 />
895 </div>
896 {customerSearch && (
897 <button
898 onClick={() => {
899 setCustomerSearch('');
900 setCustomers([]);
901 }}
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"
903 >
904 <Icon name="close" size={20} /> Clear
905 </button>
906 )}
907 </div>
908
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">
912 {customerLoading ? (
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>
916 </div>
917 ) : (
918 <>
919 {/* Cash Sale - Always First */}
920 <div
921 onClick={() => selectCustomer({
922 cid: 0,
923 name: 'Cash Sale',
924 email: '',
925 phone: '',
926 mobile: '',
927 accountBalance: 0,
928 address1: '',
929 address2: '',
930 city: '',
931 state: '',
932 postcode: '',
933 country: '',
934 deliveryAddress1: '',
935 deliveryAddress2: '',
936 deliveryCity: '',
937 deliveryState: '',
938 deliveryPostcode: '',
939 deliveryCountry: ''
940 })}
941 className="p-4 border-b-2 border-border hover:bg-surface-2 cursor-pointer bg-surface-2/50 transition-all group"
942 >
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>
946 </div>
947 <div className="text-sm text-muted mt-1">No customer account needed - Perfect for casual purchases</div>
948 </div>
949
950 {/* Search Results */}
951 {customers.map((customer) => (
952 <div
953 key={customer.cid}
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"
956 >
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)}
964 </div>
965 )}
966 </div>
967 </div>
968 ))}
969 </>
970 )}
971 </div>
972 )}
973 </div>
974 )}
975
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
985 </h3>
986 <button
987 onClick={(e) => {
988 e.preventDefault();
989 setEditingAddress({...selectedCustomer});
990 setShowBillingEdit(true);
991 }}
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"
993 >
994 <Icon name="edit" size={16} /> Edit
995 </button>
996 </div>
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(', ')}
1006 </div>
1007 )}
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>
1011 )}
1012 </div>
1013 </div>
1014 </div>
1015
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
1021 </h3>
1022 <button
1023 onClick={(e) => {
1024 e.preventDefault();
1025 setEditingAddress({...selectedCustomer});
1026 setShowShippingEdit(true);
1027 }}
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"
1029 >
1030 <Icon name="edit" size={16} /> Edit
1031 </button>
1032 </div>
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(', ')}
1042 </div>
1043 )}
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>
1047 )}
1048 </div>
1049 </div>
1050 </div>
1051 </div>
1052
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
1057 </label>
1058 <input
1059 type="text"
1060 value={orderNumber}
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"
1064 />
1065 </div>
1066
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>
1073 </label>
1074 <textarea
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"
1078 rows={3}
1079 placeholder="Enter comments visible to the customer..."
1080 />
1081 <div className="flex gap-2 mt-2 lg:mt-3">
1082 <button
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"
1085 >
1086 <Icon name="receipt" size={16} /> <span className="hidden sm:inline">Receipt Options</span><span className="sm:hidden">Receipt</span>
1087 </button>
1088 </div>
1089 </div>
1090
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>
1095 </label>
1096 <textarea
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"
1100 rows={3}
1101 placeholder="Enter internal notes (not visible to customer)..."
1102 />
1103 </div>
1104 </div>
1105 </div>
1106 )}
1107 </div>
1108
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
1115 </h2>
1116 <button
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"
1119 >
1120 <Icon name="print" size={20} /> Printers
1121 </button>
1122 </div>
1123
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" />
1130 </div>
1131 <input
1132 type="text"
1133 placeholder="Search products..."
1134 value={searchTerm}
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"
1137 />
1138 </div>
1139
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" />
1144 </div>
1145 <input
1146 ref={barcodeInputRef}
1147 type="text"
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"
1150 value={barcode}
1151 onChange={(e) => setBarcode(e.target.value)}
1152 onKeyDown={async (e) => {
1153 if (e.key === 'Enter' && barcode) {
1154 e.preventDefault();
1155 // Try to find in current products first
1156 let product = products.find(p => p.barcode === barcode);
1157
1158 // If not found, search by barcode via API
1159 if (!product) {
1160 setLoading(true);
1161 type ApiResult = {
1162 success: boolean;
1163 data?: any;
1164 error?: string;
1165 };
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)
1169 ? 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)) {
1175 product = {
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
1182 };
1183 }
1184 }
1185 }
1186 setLoading(false);
1187 }
1188
1189 if (product) {
1190 addToSale(product);
1191 setBarcode(''); // Clear barcode after successful scan
1192 } else {
1193 alert(`Product not found for barcode: ${barcode}`);
1194 }
1195 }
1196 }}
1197 />
1198 </div>
1199 </div>
1200
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">
1204 {loading ? (
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>
1208 </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>
1214 </div>
1215 ) : (
1216 <div className="divide-y divide-slate-200">
1217 {products.map((product) => (
1218 <button
1219 key={product.pid}
1220 onClick={() => {
1221 addToSale(product);
1222 setSearchTerm(''); // Clear search after adding
1223 }}
1224 className="w-full p-4 hover:bg-surface-2 transition-colors text-left flex items-center justify-between group"
1225 >
1226 <div className="flex-1">
1227 <div className="font-semibold text-text group-hover:text-brand transition-colors">
1228 {product.name}
1229 </div>
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}
1234 </span>
1235 )}
1236 {product.manufacturerCode && (
1237 <span className="flex items-center gap-1">
1238 <Icon name="local_offer" size={16} /> {product.manufacturerCode}
1239 </span>
1240 )}
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}
1244 </span>
1245 )}
1246 </div>
1247 </div>
1248 <div className="text-right ml-4">
1249 <div className="text-xl font-bold text-brand">
1250 ${product.sellprice.toFixed(2)}
1251 </div>
1252 <div className="text-xs text-slate-500 mt-1">
1253 Click to add
1254 </div>
1255 </div>
1256 </button>
1257 ))}
1258 </div>
1259 )}
1260 </div>
1261 )}
1262
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">
1271 <tr>
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>
1281 </tr>
1282 </thead>
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;
1287
1288 return (
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>
1294 {item.isReturn && (
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
1297 </span>
1298 )}
1299 </div>
1300 </div>
1301 </td>
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 ? (
1305 <input
1306 type="text"
1307 value={tempSerialNumber}
1308 onChange={(e) => setTempSerialNumber(e.target.value)}
1309 onKeyDown={(e) => {
1310 if (e.key === 'Enter') {
1311 updateSerialNumber(item.pid, tempSerialNumber);
1312 } else if (e.key === 'Escape') {
1313 setEditingSerialPid(null);
1314 setTempSerialNumber('');
1315 }
1316 }}
1317 onBlur={() => {
1318 if (tempSerialNumber.trim()) {
1319 updateSerialNumber(item.pid, tempSerialNumber);
1320 } else {
1321 setEditingSerialPid(null);
1322 setTempSerialNumber('');
1323 }
1324 }}
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"
1327 autoFocus
1328 />
1329 ) : (
1330 <button
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'
1334 }`}
1335 >
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" />
1339 </button>
1340 )}
1341 </td>
1342 <td className="px-4 py-4">
1343 <div className="flex items-center justify-center">
1344 <input
1345 type="number"
1346 value={item.qty}
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"
1349 />
1350 </div>
1351 </td>
1352 <td className="px-4 py-4 text-right font-semibold text-text">
1353 ${discountedPrice.toFixed(2)}
1354 </td>
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)}%` : '—'}
1358 </span>
1359 </td>
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)}
1364 </span>
1365 </td>
1366 <td className="px-4 py-4">
1367 <div className="flex gap-2 justify-center flex-wrap">
1368 <button
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"
1371 title="Delete item"
1372 >
1373 <Icon name="delete" size={16} />
1374 </button>
1375 <button
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"
1379 >
1380 <Icon name="undo" size={16} />
1381 </button>
1382 <button
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"
1386 >
1387 <Icon name="edit" size={16} />
1388 </button>
1389 </div>
1390 </td>
1391 </tr>
1392 );
1393 })}
1394
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):
1399 </td>
1400 <td className="px-4 py-4 text-right font-extrabold text-3xl text-success">
1401 ${total.toFixed(2)}
1402 </td>
1403 <td colSpan={2}></td>
1404 </tr>
1405
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
1412 </h3>
1413 <div className="grid grid-cols-2 md:grid-cols-4 gap-3">
1414 {paymentMethods.map((method) => (
1415 <button
1416 key={method.code}
1417 onClick={() => processSale(method.name)}
1418 disabled={loading}
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"
1420 >
1421 <span className="text-3xl">{method.icon}</span>
1422 <span>{method.name}</span>
1423 </button>
1424 ))}
1425 </div>
1426 </div>
1427
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
1431 </h4>
1432 <div className="flex gap-3 flex-wrap">
1433 <button
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"
1435 >
1436 <Icon name="inventory_2" size={20} /> Save to Picking
1437 </button>
1438 <button
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"
1440 >
1441 <Icon name="warning" size={20} /> Mark Incomplete
1442 </button>
1443 <button
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"
1445 >
1446 <Icon name="description" size={20} /> Job Docket
1447 </button>
1448 </div>
1449 </div>
1450 </td>
1451 </tr>
1452 </tbody>
1453 </table>
1454 </div>
1455
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;
1461
1462 return (
1463 <div
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`}
1466 >
1467 {/* Card Header */}
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>
1473 )}
1474 </div>
1475 <div className="text-right">
1476 <div className="text-xl font-bold text-white whitespace-nowrap">${item.total.toFixed(2)}</div>
1477 </div>
1478 </div>
1479
1480 {/* Card Body */}
1481 <div className="p-3 space-y-2">
1482 {/* Qty and Price */}
1483 <div className="grid grid-cols-2 gap-2 text-sm">
1484 <div>
1485 <div className="text-xs text-muted font-semibold">Qty</div>
1486 <input
1487 type="number"
1488 value={item.qty}
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"
1491 />
1492 </div>
1493 <div>
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>
1496 </div>
1497 </div>
1498
1499 {/* Actions */}
1500 <div className="flex gap-2">
1501 <button
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"
1504 >
1505 <Icon name="edit" size={14} /> Edit
1506 </button>
1507 <button
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`}
1510 >
1511 <Icon name="undo" size={14} />
1512 </button>
1513 <button
1514 onClick={() => removeFromSale(item.pid)}
1515 className="px-2 py-1.5 bg-danger hover:opacity-90 text-surface rounded font-semibold text-xs"
1516 >
1517 <Icon name="delete" size={14} />
1518 </button>
1519 </div>
1520 </div>
1521 </div>
1522 );
1523 })}
1524
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>
1530 </div>
1531 </div>
1532
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
1537 </h3>
1538 <div className="grid grid-cols-2 gap-2">
1539 {paymentMethods.map((method) => (
1540 <button
1541 key={method.code}
1542 onClick={() => processSale(method.name)}
1543 disabled={loading}
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"
1545 >
1546 <span className="text-xl">{method.icon}</span>
1547 <span>{method.name}</span>
1548 </button>
1549 ))}
1550 </div>
1551 </div>
1552 </div>
1553 </div>
1554 ) : (
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>
1559 </div>
1560 )}
1561
1562 {loading && (
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>
1566 </div>
1567 )}
1568 </div>
1569 </div>
1570 </div>
1571
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>
1578 <button
1579 onClick={() => { setShowCustomerModal(false); setCustomerSearch(''); }}
1580 className="text-slate-400 hover:text-slate-600 text-4xl font-bold transition-colors"
1581 >
1582 ×
1583 </button>
1584 </div>
1585
1586 <input
1587 type="text"
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"
1592 autoFocus
1593 />
1594
1595 {customerLoading ? (
1596 <div className="text-center py-8">Loading customers...</div>
1597 ) : (
1598 <div className="space-y-2">
1599 {customers
1600 .filter(c =>
1601 !customerSearch ||
1602 c.name.toLowerCase().includes(customerSearch.toLowerCase()) ||
1603 (c.email && c.email.toLowerCase().includes(customerSearch.toLowerCase())) ||
1604 (c.phone && c.phone.includes(customerSearch))
1605 )
1606 .map((customer) => (
1607 <div
1608 key={customer.cid}
1609 onClick={() => selectCustomer(customer)}
1610 className="p-4 border rounded-lg hover:bg-brand/10 cursor-pointer"
1611 >
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>}
1615 </div>
1616 ))}
1617 </div>
1618 )}
1619 </div>
1620 </div>
1621 )}
1622
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'}
1630 </h2>
1631 <button
1632 onClick={() => {
1633 setShowPrinterSearch(false);
1634 setPrinterSearchTerm("");
1635 setPrinterResults([]);
1636 setSelectedPrinter(null);
1637 setPrinterCartridges([]);
1638 }}
1639 className="text-slate-400 hover:text-slate-600 text-3xl font-bold transition-colors"
1640 >
1641 ×
1642 </button>
1643 </div>
1644
1645 {!selectedPrinter ? (
1646 <>
1647 <input
1648 type="text"
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"
1653 autoFocus
1654 />
1655
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>
1660 </div>
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>
1665 </div>
1666 ) : (
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
1670 </p>
1671 {printerResults.map((product) => (
1672 <button
1673 key={product.pid}
1674 onClick={() => {
1675 setSelectedPrinter(product);
1676
1677 // Try to use stored relationships first
1678 const relationship = printerCartridgeRelationships.find(
1679 r => r.printerId === product.pid
1680 );
1681
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)
1687 );
1688 console.log('Found cartridges from relationships:', cartridges.length);
1689 setPrinterCartridges(cartridges);
1690 } else {
1691 // Fallback to heuristic matching if no relationships stored
1692 console.log('No stored relationships found, using heuristic matching');
1693
1694 const printerText = `${product.name} ${product.description || ''} ${product.manufacturerCode || ''}`;
1695 console.log('Selected printer:', printerText);
1696
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() : '';
1700
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) || [];
1704
1705 // Extract core model numbers
1706 const coreNumbers = printerText.match(/\b\d{4,5}\b/g) || [];
1707
1708 console.log('Detected brand:', brand);
1709 console.log('Detected printer models:', models);
1710 console.log('Core model numbers:', coreNumbers);
1711
1712 // Filter for cartridge products
1713 const normalize = (str: string) => str.replace(/[-\s]/g, '').toLowerCase();
1714
1715 const cartridges = allProducts.filter(p => {
1716 const productText = `${p.name} ${p.description || ''} ${p.manufacturerCode || ''}`;
1717 const lowerText = productText.toLowerCase();
1718
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;
1722
1723 // Must have matching brand
1724 if (!brand || !lowerText.includes(brand)) return false;
1725
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);
1731 });
1732
1733 // Also check if core model numbers appear
1734 const hasCoreNumber = coreNumbers.some(num => productText.includes(num));
1735
1736 return hasExactModel || hasCoreNumber;
1737 });
1738
1739 console.log('Found cartridges:', cartridges.length);
1740 setPrinterCartridges(cartridges.slice(0, 100));
1741 }
1742 }}
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"
1744 >
1745 <div className="flex-1">
1746 <div className="font-bold text-text mb-1 group-hover:text-brand2 transition-colors">
1747 {product.name}
1748 </div>
1749 {product.description && (
1750 <div className="text-sm text-muted mb-2">{product.description}</div>
1751 )}
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}
1756 </span>
1757 )}
1758 </div>
1759 </div>
1760 <div className="text-right ml-4">
1761 <div className="text-sm text-info font-semibold">
1762 View Cartridges →
1763 </div>
1764 </div>
1765 </button>
1766 ))}
1767 </div>
1768 )}
1769 </>
1770 ) : (
1771 <>
1772 <button
1773 onClick={() => {
1774 setSelectedPrinter(null);
1775 setPrinterCartridges([]);
1776 }}
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"
1778 >
1779 ← Back to Printer Search
1780 </button>
1781
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>
1787 </div>
1788 ) : (
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' : ''}
1792 </p>
1793 {printerCartridges.map((product) => (
1794 <button
1795 key={product.pid}
1796 onClick={() => {
1797 addToSale(product);
1798 setShowPrinterSearch(false);
1799 setPrinterSearchTerm("");
1800 setPrinterResults([]);
1801 setSelectedPrinter(null);
1802 setPrinterCartridges([]);
1803 }}
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"
1805 >
1806 <div className="flex-1">
1807 <div className="font-bold text-text mb-1 group-hover:text-success transition-colors">
1808 {product.name}
1809 </div>
1810 {product.description && (
1811 <div className="text-sm text-muted mb-2">{product.description}</div>
1812 )}
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}
1817 </span>
1818 )}
1819 {product.manufacturerCode && (
1820 <span className="flex items-center gap-1">
1821 <Icon name="local_offer" size={14} /> {product.manufacturerCode}
1822 </span>
1823 )}
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}
1827 </span>
1828 )}
1829 </div>
1830 </div>
1831 <div className="text-right ml-4">
1832 <div className="text-xl font-bold text-success">
1833 ${product.sellprice.toFixed(2)}
1834 </div>
1835 <div className="text-xs text-slate-500 mt-1">
1836 Click to add
1837 </div>
1838 </div>
1839 </button>
1840 ))}
1841 </div>
1842 )}
1843 </>
1844 )}
1845 </div>
1846 </div>
1847 )}
1848
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
1855 </h2>
1856
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>
1860 </div>
1861
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
1865 </label>
1866 <input
1867 type="number"
1868 step="0.01"
1869 value={tempPrice}
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"
1872 />
1873 </div>
1874
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 %
1878 </label>
1879 <input
1880 type="number"
1881 step="0.1"
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"
1885 />
1886 </div>
1887
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">
1891 <div>
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>
1894 </div>
1895 <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';
1901 })()}`}>
1902 ${(() => {
1903 const discountedPrice = tempDiscount ? tempPrice * (1 - tempDiscount / 100) : tempPrice;
1904 const margin = (discountedPrice - editingLineItem.cost) * editingLineItem.qty;
1905 return margin.toFixed(2);
1906 })()}
1907 </div>
1908 </div>
1909 <div>
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';
1915 })()}`}>
1916 {(() => {
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);
1920 })()}%
1921 </div>
1922 </div>
1923 <div>
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">
1926 ${(() => {
1927 const discountedPrice = tempDiscount ? tempPrice * (1 - tempDiscount / 100) : tempPrice;
1928 return (discountedPrice * editingLineItem.qty).toFixed(2);
1929 })()}
1930 </div>
1931 </div>
1932 </div>
1933 </div>
1934 )}
1935
1936 <div className="flex gap-3">
1937 <button
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"
1940 >
1941 <Icon name="check" size={20} className="inline-block mr-2" /> Save Changes
1942 </button>
1943 <button
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"
1946 >
1947 <Icon name="close" size={20} className="inline-block mr-2" /> Cancel
1948 </button>
1949 </div>
1950 </div>
1951 </div>
1952 )}
1953
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>
1960 <button
1961 onClick={() => setShowParkedSales(false)}
1962 className="text-muted hover:text-text text-2xl"
1963 >
1964 ×
1965 </button>
1966 </div>
1967
1968 {parkedSales.length === 0 ? (
1969 <div className="text-center py-8 text-muted">No parked sales</div>
1970 ) : (
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">
1975 <div>
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>}
1980 </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>
1984 </div>
1985 </div>
1986 <button
1987 onClick={() => { unparkSale(sale); setShowParkedSales(false); }}
1988 className="w-full px-4 py-2 bg-success text-surface rounded hover:opacity-90"
1989 >
1990 Restore Sale
1991 </button>
1992 </div>
1993 ))}
1994 </div>
1995 )}
1996 </div>
1997 </div>
1998 )}
1999
2000 {/* Past Sales Modal */}
2001 {showPastSales && (
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>
2006 <button
2007 onClick={() => setShowPastSales(false)}
2008 className="text-muted hover:text-text text-2xl"
2009 >
2010 ×
2011 </button>
2012 </div>
2013
2014 {pastSales.length === 0 ? (
2015 <div className="text-center py-8 text-muted">No past sales found</div>
2016 ) : (
2017 <div className="overflow-x-auto">
2018 <table className="w-full text-sm">
2019 <thead className="bg-surface-2 border-b-2">
2020 <tr>
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>
2026 </tr>
2027 </thead>
2028 <tbody>
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>
2036 </tr>
2037 ))}
2038 </tbody>
2039 </table>
2040 </div>
2041 )}
2042 </div>
2043 </div>
2044 )}
2045
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>
2051
2052 <div className="mb-4">
2053 <label className="flex items-center space-x-2 mb-3">
2054 <input
2055 type="checkbox"
2056 checked={printReceipt}
2057 onChange={(e) => setPrintReceipt(e.target.checked)}
2058 className="w-5 h-5"
2059 />
2060 <span className="font-semibold">Print Receipt</span>
2061 </label>
2062
2063 <label className="flex items-center space-x-2">
2064 <input
2065 type="checkbox"
2066 checked={emailReceipt}
2067 onChange={(e) => setEmailReceipt(e.target.checked)}
2068 className="w-5 h-5"
2069 />
2070 <span className="font-semibold">Email Receipt</span>
2071 </label>
2072 </div>
2073
2074 {emailReceipt && (
2075 <div className="mb-4">
2076 <label className="block font-semibold mb-2">Email Address</label>
2077 <input
2078 type="email"
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"
2083 />
2084 </div>
2085 )}
2086
2087 <div className="flex gap-2">
2088 <button
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"
2092 >
2093 Complete Sale
2094 </button>
2095 <button
2096 onClick={() => { setShowReceiptOptions(false); setSelectedPaymentMethod(''); }}
2097 className="flex-1 px-4 py-2 bg-surface-2 rounded hover:opacity-90"
2098 >
2099 Cancel
2100 </button>
2101 </div>
2102 </div>
2103 </div>
2104 )}
2105
2106 {/* End of Day Modal */}
2107 {showEndOfDay && (
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>
2112 <button
2113 onClick={() => setShowEndOfDay(false)}
2114 className="text-muted hover:text-text text-2xl"
2115 >
2116 ×
2117 </button>
2118 </div>
2119
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>
2124 </div>
2125
2126 <button
2127 onClick={() => setShowEndOfDay(false)}
2128 className="w-full px-4 py-2 bg-brand text-surface rounded hover:opacity-90"
2129 >
2130 Close
2131 </button>
2132 </div>
2133 </div>
2134 </div>
2135 )}
2136
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>
2143 <button
2144 onClick={() => setShowBillingEdit(false)}
2145 className="text-muted hover:text-text text-2xl"
2146 >
2147 ×
2148 </button>
2149 </div>
2150
2151 <div className="space-y-3">
2152 <div>
2153 <label className="block text-sm font-semibold mb-1">Address Line 1</label>
2154 <input
2155 type="text"
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"
2159 />
2160 </div>
2161 <div>
2162 <label className="block text-sm font-semibold mb-1">Address Line 2</label>
2163 <input
2164 type="text"
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"
2168 />
2169 </div>
2170 <div className="grid grid-cols-2 gap-3">
2171 <div>
2172 <label className="block text-sm font-semibold mb-1">City</label>
2173 <input
2174 type="text"
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"
2178 />
2179 </div>
2180 <div>
2181 <label className="block text-sm font-semibold mb-1">State</label>
2182 <input
2183 type="text"
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"
2187 />
2188 </div>
2189 </div>
2190 <div className="grid grid-cols-2 gap-3">
2191 <div>
2192 <label className="block text-sm font-semibold mb-1">Postcode</label>
2193 <input
2194 type="text"
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"
2198 />
2199 </div>
2200 <div>
2201 <label className="block text-sm font-semibold mb-1">Country</label>
2202 <input
2203 type="text"
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"
2207 />
2208 </div>
2209 </div>
2210 <div className="flex gap-3 pt-4">
2211 <button
2212 onClick={async () => {
2213 if (editingAddress) {
2214 try {
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
2222 } as any);
2223
2224 if (result.success) {
2225 setSelectedCustomer(editingAddress);
2226 setShowBillingEdit(false);
2227 alert('✅ Billing address updated successfully!');
2228 } else {
2229 alert('❌ Failed to update billing address: ' + (result.error || 'Unknown error'));
2230 }
2231 } catch (error) {
2232 console.error('Failed to update billing address:', error);
2233 alert('❌ Failed to update billing address. Please try again.');
2234 }
2235 }
2236 }}
2237 className="flex-1 px-4 py-2 bg-brand text-surface rounded hover:opacity-90 font-semibold"
2238 >
2239 Save Changes
2240 </button>
2241 <button
2242 onClick={() => setShowBillingEdit(false)}
2243 className="flex-1 px-4 py-2 bg-surface-2 text-text rounded hover:opacity-90 font-semibold"
2244 >
2245 Cancel
2246 </button>
2247 </div>
2248 </div>
2249 </div>
2250 </div>
2251 )}
2252
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>
2259 <button
2260 onClick={() => setShowShippingEdit(false)}
2261 className="text-muted hover:text-text text-2xl"
2262 >
2263 ×
2264 </button>
2265 </div>
2266
2267 <div className="mb-4">
2268 <button
2269 onClick={() => {
2270 if (editingAddress && selectedCustomer) {
2271 setEditingAddress({
2272 ...editingAddress,
2273 deliveryAddress1: selectedCustomer.billingAddress1,
2274 deliveryAddress2: selectedCustomer.billingAddress2,
2275 deliveryCity: selectedCustomer.billingCity,
2276 deliveryState: selectedCustomer.billingState,
2277 deliveryPostcode: selectedCustomer.billingPostcode,
2278 deliveryCountry: selectedCustomer.billingCountry
2279 });
2280 }
2281 }}
2282 className="w-full px-4 py-2 bg-surface-2 hover:bg-surface-2/80 rounded font-semibold"
2283 >
2284 Copy from Billing Address
2285 </button>
2286 </div>
2287
2288 <div className="space-y-3">
2289 <div>
2290 <label className="block text-sm font-semibold mb-1">Address Line 1</label>
2291 <input
2292 type="text"
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"
2296 />
2297 </div>
2298 <div>
2299 <label className="block text-sm font-semibold mb-1">Address Line 2</label>
2300 <input
2301 type="text"
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"
2305 />
2306 </div>
2307 <div className="grid grid-cols-2 gap-3">
2308 <div>
2309 <label className="block text-sm font-semibold mb-1">City</label>
2310 <input
2311 type="text"
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"
2315 />
2316 </div>
2317 <div>
2318 <label className="block text-sm font-semibold mb-1">State</label>
2319 <input
2320 type="text"
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"
2324 />
2325 </div>
2326 </div>
2327 <div className="grid grid-cols-2 gap-3">
2328 <div>
2329 <label className="block text-sm font-semibold mb-1">Postcode</label>
2330 <input
2331 type="text"
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"
2335 />
2336 </div>
2337 <div>
2338 <label className="block text-sm font-semibold mb-1">Country</label>
2339 <input
2340 type="text"
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"
2344 />
2345 </div>
2346 </div>
2347 <div className="flex gap-3 pt-4">
2348 <button
2349 onClick={async () => {
2350 if (editingAddress) {
2351 try {
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
2359 } as any);
2360
2361 if (result.success) {
2362 setSelectedCustomer(editingAddress);
2363 setShowShippingEdit(false);
2364 alert('✅ Delivery address updated successfully!');
2365 } else {
2366 alert('❌ Failed to update delivery address: ' + (result.error || 'Unknown error'));
2367 }
2368 } catch (error) {
2369 console.error('Failed to update delivery address:', error);
2370 alert('❌ Failed to update delivery address. Please try again.');
2371 }
2372 }
2373 }}
2374 className="flex-1 px-4 py-2 bg-brand text-surface rounded hover:opacity-90 font-semibold"
2375 >
2376 Save Changes
2377 </button>
2378 <button
2379 onClick={() => setShowShippingEdit(false)}
2380 className="flex-1 px-4 py-2 bg-surface-2 text-text rounded hover:opacity-90 font-semibold"
2381 >
2382 Cancel
2383 </button>
2384 </div>
2385 </div>
2386 </div>
2387 </div>
2388 )}
2389
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
2396 </h2>
2397
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>
2401 </div>
2402
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
2406 </label>
2407 <input
2408 type="text"
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"
2413 autoFocus
2414 onKeyDown={(e) => {
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('');
2421 }
2422 }}
2423 />
2424 <p className="mt-2 text-sm text-slate-500">Press Enter to add, Esc to cancel</p>
2425 </div>
2426
2427 <div className="flex gap-3">
2428 <button
2429 onClick={() => {
2430 if (serialNumber.trim()) {
2431 addToSaleWithSerial(serialNumber.trim());
2432 }
2433 }}
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"
2436 >
2437 <Icon name="check" size={20} className="inline-block mr-2" /> Add to Sale
2438 </button>
2439 <button
2440 onClick={() => {
2441 setShowSerialModal(false);
2442 setPendingSerialProduct(null);
2443 setSerialNumber('');
2444 }}
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"
2446 >
2447 <Icon name="close" size={20} className="inline-block mr-2" /> Cancel
2448 </button>
2449 </div>
2450 </div>
2451 </div>
2452 )}
2453 </div>
2454);
2455}