3import { use, useEffect, useState } from 'react';
8 f205: number; // Total Price
9 f212: string; // Description
11 f303: number; // Total Inc Tax
12 f304: number; // Total Ex Tax
18 f201: number; // Amount
20 f900: string; // Description
24 f100: number; // Sale ID
25 f101: string; // End Date
26 f102: number; // Total
27 f108: number; // Status/Phase
28 f109: string; // Start Date
29 f117: number; // Customer ID
30 f125: number; // Location ID
31 f130: string; // External ID
32 f133: string; // Comments
33 LOCN?: Array<{ f101: string }>; // Location name
34 STAF?: Array<{ f101: string }>; // Staff name
35 CUST?: Array<{ f101: string; Name?: string }>; // Customer
40 f302?: string; // Instructions
41 f303?: string; // Phone
42 f304?: string; // Name
43 f305?: string; // Email
44 f420?: string; // Address
45 f421?: string; // City
46 f422?: string; // Country
47 f423?: string; // Postcode
51export default function SaleDetailPage({ params }: { params: Promise<{ id: string }> }) {
52 const { id } = use(params);
53 const [sale, setSale] = useState<SaleDetail | null>(null);
54 const [loading, setLoading] = useState(true);
55 const [error, setError] = useState<string | null>(null);
56 const [editMode, setEditMode] = useState(false);
57 const [saving, setSaving] = useState(false);
58 const [locations, setLocations] = useState<Array<{ f100: number; f101: string }>>([]);
59 const [editData, setEditData] = useState({
61 f7120: '', // Picking comments
62 f7123: '', // Customer comments
65 const [editedItems, setEditedItems] = useState<any[]>([]);
66 const [showAddItem, setShowAddItem] = useState(false);
67 const [productSearch, setProductSearch] = useState('');
68 const [searchIn, setSearchIn] = useState<'all' | 'name' | 'barcode' | 'pid'>('all');
69 const [searchResults, setSearchResults] = useState<any[]>([]);
70 const [searching, setSearching] = useState(false);
78 const timer = setTimeout(() => {
80 searchProducts(productSearch);
84 return () => clearTimeout(timer);
87 const loadLocations = async () => {
89 const response = await fetch('/api/v1/locations');
91 const data = await response.json();
92 setLocations(data.data || []);
95 console.error('Failed to load locations:', error);
99 const loadSale = async () => {
103 console.log(`[Sale Detail Page] Fetching sale ${id}`);
104 const response = await fetch(`/api/v1/sales/${id}/details`);
105 console.log(`[Sale Detail Page] Response status:`, response.status, response.statusText);
107 const text = await response.text();
108 console.log(`[Sale Detail Page] Response text:`, text);
112 data = JSON.parse(text);
114 console.error('[Sale Detail Page] Failed to parse JSON:', e);
115 setError('Invalid response from server');
119 console.log('[Sale Detail Page] Parsed data:', data);
121 if (response.ok && data.success) {
123 // Initialize edit data with current values
125 f7102: data.data.f7102 || 0,
126 f7120: data.data.f7120 || '',
127 f7123: data.data.f7123 || '',
128 f108: data.data.f108 || 200
130 // Initialize line items for editing
131 setEditedItems(data.data.BIG1 || []);
133 setError(data.error || 'Failed to load sale');
134 console.error('API error:', data);
137 console.error('Failed to load sale:', error);
138 setError('Network error loading sale');
144 const updateLineItem = (index: number, qty: number) => {
145 setEditedItems(prev => {
146 const newItems = [...prev];
147 newItems[index] = { ...newItems[index], f101: qty };
152 const removeLineItem = (index: number) => {
153 if (confirm('Remove this item from the sale?')) {
154 setEditedItems(prev => prev.filter((_, i) => i !== index));
158 const searchProducts = async (query: string) => {
159 if (query.length < 2) {
160 setSearchResults([]);
166 const term = query.trim();
168 // Try using EnglishQuery parameter which Fieldpine OpenAPI uses for text search
169 const response = await fetch(`/api/v1/openapi/products?EnglishQuery=${encodeURIComponent(term)}&limit=50`);
171 console.log('[Sale Search] Searching for:', term);
172 console.log('[Sale Search] Response status:', response.status);
175 const data = await response.json();
176 console.log('[Sale Search] Full API response:', data);
178 // Handle OpenAPI response structure - can be nested differently based on query
179 let products = data?.data;
181 // If data.data is not an array, try going deeper
182 if (!Array.isArray(products)) {
183 console.log('[Sale Search] data.data is not array, checking data.data.data:', products?.data);
184 products = products?.data;
187 // If still not array, try data.data.data.Product
188 if (!Array.isArray(products)) {
189 console.log('[Sale Search] Checking data.data.data.Product:', products?.Product);
190 products = products?.Product;
193 // If still not array, default to empty
194 if (!Array.isArray(products)) {
195 console.log('[Sale Search] Could not find products array, defaulting to empty');
199 console.log('[Sale Search] Final products array:', products);
200 console.log('[Sale Search] Products found:', products.length);
202 if (Array.isArray(products) && products.length > 0) {
203 console.log('[Sale Search] First product sample:', products[0]);
206 // Filter results based on search type if not 'all'
207 let filtered = Array.isArray(products) ? products : [];
208 if (searchIn !== 'all' && filtered.length > 0) {
209 const originalCount = filtered.length;
210 filtered = filtered.filter((p: any) => {
213 return (p.Description || p.Name || '').toLowerCase().includes(term.toLowerCase());
215 return (p.Plu || p.PLU || p.Barcode || '').toLowerCase().includes(term.toLowerCase());
217 return p.Pid === Number(term);
222 console.log('[Sale Search] Filtered from', originalCount, 'to', filtered.length, 'items');
225 setSearchResults(filtered.slice(0, 20));
227 const errorText = await response.text();
228 console.error('[Sale Search] API error:', response.status, errorText);
229 setSearchResults([]);
232 console.error('[Sale Search] Exception:', error);
233 setSearchResults([]);
239 const addProductToSale = (product: any) => {
241 f100: product.Pid, // Product ID
243 f102: product.Price || product.SellPrice || '0', // Price
244 f103: product.Description || product.Name || 'Unknown Product' // Description
247 setEditedItems(prev => [...prev, newItem]);
248 setShowAddItem(false);
249 setProductSearch('');
250 setSearchResults([]);
253 const handleSave = async () => {
256 const updatePayload = {
258 lineItems: editedItems // Include edited line items
261 const response = await fetch(`/api/v1/sales/${id}`, {
263 headers: { 'Content-Type': 'application/json' },
264 body: JSON.stringify(updatePayload)
268 // Update local sale state with edited items
269 setSale(prev => prev ? { ...prev, ...editData, BIG1: editedItems } : null);
272 alert('Failed to save changes');
275 console.error('Failed to save:', error);
276 alert('Error saving changes');
282 const formatDate = (dateStr: string | undefined) => {
283 if (!dateStr) return '';
285 if (dateStr.includes('|')) {
286 const parts = dateStr.split('|');
287 const year = parseInt(parts[0]);
288 const month = parseInt(parts[1]);
289 const day = parseInt(parts[2]);
290 const hour = parseInt(parts[3] || '0');
291 const minute = parseInt(parts[4] || '0');
293 const date = new Date(year, month - 1, day, hour, minute);
294 const monthName = date.toLocaleDateString('en-US', { month: 'short' });
295 const timeStr = `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
297 return `${day}-${monthName}-${year} ${timeStr}`;
300 const date = new Date(dateStr);
301 if (isNaN(date.getTime())) return dateStr;
303 const day = date.getDate();
304 const month = date.toLocaleDateString('en-US', { month: 'short' });
305 const year = date.getFullYear();
306 const time = date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false });
308 return `${day}-${month}-${year} ${time}`;
310 return dateStr || '';
314 const getStatusText = (status: number) => {
316 case 0: return 'Active';
317 case 1: return 'Complete';
318 case 5: return 'Parked';
319 case 8: return 'Internal/WriteOff';
320 case 200: return 'Picking';
321 case 202: return 'Awaiting Picking';
322 case 10000: return 'Deleted';
323 case 10001: return 'Quotation';
324 case 10002: return 'Deleted';
325 default: return `Status ${status}`;
329 const fieldpineUrl = `https://iig.cwanz.online/report/pos/sales/fieldpine/SingleSale.htm?sid=${id}`;
333 <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
334 <div className="text-center py-12">
335 <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#00946b] mx-auto"></div>
336 <p className="mt-4 text-muted">Loading sale details...</p>
344 <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
345 <div className="bg-red-50 border border-red-200 rounded-lg p-6">
346 <h2 className="text-lg font-semibold text-red-800 mb-2">Error Loading Sale</h2>
347 <p className="text-red-600">{error}</p>
355 <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
356 <div className="text-center py-12">
357 <p className="text-muted">Sale not found</p>
364 <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
366 <div className="mb-6 flex items-center justify-between">
368 <h1 className="text-3xl font-bold text-text">Sale #{sale.f100}</h1>
369 <p className="mt-1 text-sm text-muted">
370 {sale.LOCN?.[0]?.f101 || `Location# ${sale.f125}`} ({formatDate(sale.f101)})
373 <div className="flex gap-2">
377 onClick={() => setEditMode(false)}
378 className="px-4 py-2 bg-surface-2 text-text rounded-md hover:bg-surface-2 transition-colors"
386 className="px-4 py-2 bg-[#00946b] text-white rounded-md hover:bg-[#007a59] transition-colors disabled:opacity-50"
388 {saving ? 'Saving...' : 'Save Changes'}
393 onClick={() => setEditMode(true)}
394 className="px-4 py-2 bg-[#00946b] text-white rounded-md hover:bg-[#007a59] transition-colors"
403 <div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
404 <div className="bg-surface rounded-lg shadow p-4">
405 <div className="text-sm font-medium text-muted">Started</div>
406 <div className="mt-1 text-lg text-text">{formatDate(sale.f109)}</div>
409 <div className="bg-surface rounded-lg shadow p-4">
410 <div className="text-sm font-medium text-muted">Status</div>
413 value={editData.f108}
414 onChange={(e) => setEditData(prev => ({ ...prev, f108: Number(e.target.value) }))}
415 className="mt-1 block w-full rounded-md border-border shadow-sm focus:border-[#00946b] focus:ring-[#00946b] text-sm"
417 <option value={200}>Awaiting Picking</option>
418 <option value={202}>Picking</option>
419 <option value={1}>Complete</option>
420 <option value={5}>Parked</option>
423 <div className="mt-1 text-lg font-semibold text-text">{getStatusText(sale.f108)}</div>
427 <div className="bg-surface rounded-lg shadow p-4">
428 <div className="text-sm font-medium text-muted">Teller</div>
429 <div className="mt-1 text-lg text-text">{sale.STAF?.[0]?.f101 || <i>None</i>}</div>
432 <div className="bg-surface rounded-lg shadow p-4">
433 <div className="text-sm font-medium text-muted">Customer</div>
434 <div className="mt-1 text-lg text-text">
436 <a href={`/pages/customers/${sale.f117}`} className="text-[#00946b] hover:underline">
437 {sale.CUST?.[0]?.Name || sale.CUST?.[0]?.f101 || `#${sale.f117}`}
446 {/* Editable Fields */}
448 <div className="bg-info/10 border border-info/30 rounded-lg p-6 mb-6">
449 <h3 className="text-lg font-semibold text-brand mb-4">Edit Sale Details</h3>
450 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
452 <label className="block text-sm font-medium text-text mb-1">Assigned Store</label>
454 value={editData.f7102}
455 onChange={(e) => setEditData(prev => ({ ...prev, f7102: Number(e.target.value) }))}
456 className="block w-full rounded-md border-border shadow-sm focus:border-[#00946b] focus:ring-[#00946b] text-sm"
458 <option value={0}>Not Assigned</option>
459 {locations.map(loc => (
460 <option key={loc.f100} value={loc.f100}>{loc.f101}</option>
466 <label className="block text-sm font-medium text-text mb-1">Picking Comments</label>
469 value={editData.f7120}
470 onChange={(e) => setEditData(prev => ({ ...prev, f7120: e.target.value }))}
471 placeholder="Internal picking notes..."
472 className="block w-full rounded-md border-border shadow-sm focus:border-[#00946b] focus:ring-[#00946b] text-sm"
476 <div className="md:col-span-2">
477 <label className="block text-sm font-medium text-text mb-1">Customer Comments</label>
479 value={editData.f7123}
480 onChange={(e) => setEditData(prev => ({ ...prev, f7123: e.target.value }))}
481 placeholder="Customer-visible comments..."
483 className="block w-full rounded-md border-border shadow-sm focus:border-[#00946b] focus:ring-[#00946b] text-sm"
490 {/* External ID & Comments */}
491 {(sale.f130 || sale.f133) && (
492 <div className="bg-surface rounded-lg shadow p-4 mb-6">
494 <div className="mb-2">
495 <span className="font-medium text-text">External ID:</span>{' '}
496 <span className="text-text">{sale.f130}</span>
501 <span className="font-medium text-text">Comments:</span>{' '}
502 <span className="text-brand">{sale.f133}</span>
508 {/* Delivery Details */}
509 {sale.DELI && sale.DELI.length > 0 && (
510 <div className="bg-surface rounded-lg shadow p-6 mb-6">
511 <h2 className="text-xl font-bold text-text mb-4">Delivery Details</h2>
512 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
514 <div className="text-sm font-medium text-muted">Address</div>
515 <div className="mt-1 text-text">
516 {sale.DELI[0].f300 && <div>{sale.DELI[0].f300}</div>}
517 {sale.DELI[0].f423 && <div>{sale.DELI[0].f423}</div>}
518 {sale.DELI[0].f421 && sale.DELI[0].f420 && (
519 <div>{sale.DELI[0].f421} {sale.DELI[0].f420}</div>
521 {sale.DELI[0].f422 && <div>{sale.DELI[0].f422}</div>}
525 {sale.DELI[0].f304 && (
526 <div className="mb-3">
527 <div className="text-sm font-medium text-muted">Name</div>
528 <div className="mt-1 text-text">{sale.DELI[0].f304}</div>
531 {sale.DELI[0].f303 && (
532 <div className="mb-3">
533 <div className="text-sm font-medium text-muted">Phone</div>
534 <div className="mt-1">
535 <a href={`tel:${sale.DELI[0].f303}`} className="text-[#00946b] hover:underline">
541 {sale.DELI[0].f305 && (
542 <div className="mb-3">
543 <div className="text-sm font-medium text-muted">Email</div>
544 <div className="mt-1">
545 <a href={`mailto:${sale.DELI[0].f305}`} className="text-[#00946b] hover:underline">
551 {sale.DELI[0].f302 && (
553 <div className="text-sm font-medium text-muted">Instructions</div>
554 <div className="mt-1 text-text italic">{sale.DELI[0].f302}</div>
562 {/* Items Purchased */}
563 <div className="bg-surface rounded-lg shadow mb-6">
564 <div className="px-6 py-4 border-b border-border flex items-center justify-between">
565 <h2 className="text-xl font-bold text-text">Items Purchased</h2>
568 onClick={() => setShowAddItem(!showAddItem)}
569 className="px-3 py-1 bg-[#00946b] text-white text-sm rounded-md hover:bg-[#007a59] transition-colors"
571 {showAddItem ? 'Cancel' : '+ Add Item'}
576 {/* Add Item Search */}
577 {editMode && showAddItem && (
578 <div className="px-6 py-4 bg-info/10 border-b border-info/30">
579 <div className="flex gap-4 mb-3">
580 <div className="flex-1">
581 <label className="block text-sm font-medium text-text mb-2">Search for product</label>
584 value={productSearch}
585 onChange={(e) => setProductSearch(e.target.value)}
586 placeholder="Type to search..."
587 className="block w-full rounded-md border-border shadow-sm focus:border-[#00946b] focus:ring-[#00946b] text-sm"
591 <div className="w-48">
592 <label className="block text-sm font-medium text-text mb-2">Search in</label>
595 onChange={(e) => setSearchIn(e.target.value as any)}
596 className="block w-full rounded-md border-border shadow-sm focus:border-[#00946b] focus:ring-[#00946b] text-sm"
598 <option value="all">All Fields</option>
599 <option value="name">Name</option>
600 <option value="barcode">Barcode/PLU</option>
601 <option value="pid">Product ID</option>
606 <div className="mt-2 text-sm text-muted">Searching...</div>
608 {searchResults.length > 0 && (
609 <div className="mt-2 max-h-96 overflow-y-auto border border-border rounded-md bg-surface shadow-lg">
610 <div className="grid grid-cols-1 divide-y divide-gray-100">
611 {searchResults.map((product) => (
614 onClick={() => addProductToSale(product)}
615 className="w-full px-4 py-3 text-left hover:bg-info/10 transition-colors"
617 <div className="flex items-start justify-between">
618 <div className="flex-1">
619 <div className="font-medium text-text">{product.Description || product.Name}</div>
620 <div className="text-sm text-muted mt-1">
621 <span className="inline-block mr-4">ID: {product.Pid}</span>
622 {product.Plu && <span className="inline-block mr-4">PLU: {product.Plu}</span>}
623 {product.StockLevel !== undefined && (
624 <span className={`inline-block ${product.StockLevel > 0 ? 'text-green-600' : 'text-red-600'}`}>
625 Stock: {product.StockLevel}
630 <div className="text-right ml-4">
631 <div className="text-lg font-semibold text-text">
632 ${parseFloat(product.Price || product.SellPrice || '0').toFixed(2)}
641 {productSearch.length >= 2 && searchResults.length === 0 && !searching && (
642 <div className="mt-2 p-4 bg-surface border border-border rounded-md text-center text-sm text-muted">
643 No products found. Try a different search term or filter.
649 <div className="overflow-x-auto">
650 <table className="min-w-full divide-y divide-gray-200">
651 <thead className="bg-surface-2">
653 <th className="px-6 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider">Pid#</th>
654 <th className="px-6 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider">Description</th>
655 <th className="px-6 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider">Qty</th>
656 <th className="px-6 py-3 text-right text-xs font-medium text-muted uppercase tracking-wider">Price</th>
657 {editMode && <th className="px-6 py-3 text-center text-xs font-medium text-muted uppercase tracking-wider">Actions</th>}
660 <tbody className="bg-surface divide-y divide-gray-200">
661 {(editMode ? editedItems : sale.LINE || []).length > 0 ? (
662 (editMode ? editedItems : sale.LINE || []).map((item: any, idx: number) => (
663 <tr key={idx} className={idx % 2 === 0 ? 'bg-surface' : 'bg-surface-2'}>
664 <td className="px-6 py-4 whitespace-nowrap text-sm text-text">
666 href={`/pages/products/${item.f100}`}
667 className="text-[#00946b] hover:underline"
672 <td className="px-6 py-4 text-sm text-text">{item.f103}</td>
673 <td className="px-6 py-4 whitespace-nowrap text-sm text-text">
678 onChange={(e) => updateLineItem(idx, Number(e.target.value))}
681 className="w-20 rounded border-border text-sm focus:border-[#00946b] focus:ring-[#00946b]"
687 <td className="px-6 py-4 whitespace-nowrap text-sm text-text text-right">
688 ${parseFloat(item.f102 || '0').toFixed(2)}
691 <td className="px-6 py-4 whitespace-nowrap text-center">
693 onClick={() => removeLineItem(idx)}
694 className="text-red-600 hover:text-red-800 text-sm"
704 <td colSpan={editMode ? 5 : 4} className="px-6 py-4 text-center text-sm text-muted">
710 <tfoot className="bg-surface-2">
712 <td colSpan={3} className="px-6 py-4 text-right text-sm font-semibold text-text">
715 <td className="px-6 py-4 whitespace-nowrap text-sm font-bold text-text text-right">
716 ${sale.f102?.toFixed(2)}
725 <div className="bg-surface rounded-lg shadow">
726 <div className="px-6 py-4 border-b border-border">
727 <h2 className="text-xl font-bold text-text">Payment</h2>
729 <div className="px-6 py-4">
730 <div className="text-sm text-muted mb-2">Status: {getStatusText(sale.f108)}</div>
731 <div className="text-2xl font-bold text-text">${sale.f102?.toFixed(2)}</div>
732 {sale.f108 === 200 && (
733 <div className="mt-2 inline-block px-3 py-1 bg-yellow-100 text-yellow-800 text-sm rounded-full">
737 {sale.f108 === 1 && (
738 <div className="mt-2 inline-block px-3 py-1 bg-green-100 text-green-800 text-sm rounded-full">