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';
2
3import { use, useEffect, useState } from 'react';
4
5interface SaleItem {
6 f200: number; // Pid
7 f202: number; // Qty
8 f205: number; // Total Price
9 f212: string; // Description
10 f213: string; // PLU
11 f303: number; // Total Inc Tax
12 f304: number; // Total Ex Tax
13 f305: number; // Tax
14}
15
16interface Payment {
17 f200: number; // Type
18 f201: number; // Amount
19 f204: string; // Time
20 f900: string; // Description
21}
22
23interface SaleDetail {
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
36 LINE?: SaleItem[];
37 PAYM?: Payment[];
38 DELI?: Array<{
39 f300?: string;
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
48 }>;
49}
50
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({
60 f7102: 0, // Store
61 f7120: '', // Picking comments
62 f7123: '', // Customer comments
63 f108: 200 // Status
64 });
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);
71
72 useEffect(() => {
73 loadSale();
74 loadLocations();
75 }, [id]);
76
77 useEffect(() => {
78 const timer = setTimeout(() => {
79 if (productSearch) {
80 searchProducts(productSearch);
81 }
82 }, 300);
83
84 return () => clearTimeout(timer);
85 }, [productSearch]);
86
87 const loadLocations = async () => {
88 try {
89 const response = await fetch('/api/v1/locations');
90 if (response.ok) {
91 const data = await response.json();
92 setLocations(data.data || []);
93 }
94 } catch (error) {
95 console.error('Failed to load locations:', error);
96 }
97 };
98
99 const loadSale = async () => {
100 setLoading(true);
101 setError(null);
102 try {
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);
106
107 const text = await response.text();
108 console.log(`[Sale Detail Page] Response text:`, text);
109
110 let data;
111 try {
112 data = JSON.parse(text);
113 } catch (e) {
114 console.error('[Sale Detail Page] Failed to parse JSON:', e);
115 setError('Invalid response from server');
116 return;
117 }
118
119 console.log('[Sale Detail Page] Parsed data:', data);
120
121 if (response.ok && data.success) {
122 setSale(data.data);
123 // Initialize edit data with current values
124 setEditData({
125 f7102: data.data.f7102 || 0,
126 f7120: data.data.f7120 || '',
127 f7123: data.data.f7123 || '',
128 f108: data.data.f108 || 200
129 });
130 // Initialize line items for editing
131 setEditedItems(data.data.BIG1 || []);
132 } else {
133 setError(data.error || 'Failed to load sale');
134 console.error('API error:', data);
135 }
136 } catch (error) {
137 console.error('Failed to load sale:', error);
138 setError('Network error loading sale');
139 } finally {
140 setLoading(false);
141 }
142 };
143
144 const updateLineItem = (index: number, qty: number) => {
145 setEditedItems(prev => {
146 const newItems = [...prev];
147 newItems[index] = { ...newItems[index], f101: qty };
148 return newItems;
149 });
150 };
151
152 const removeLineItem = (index: number) => {
153 if (confirm('Remove this item from the sale?')) {
154 setEditedItems(prev => prev.filter((_, i) => i !== index));
155 }
156 };
157
158 const searchProducts = async (query: string) => {
159 if (query.length < 2) {
160 setSearchResults([]);
161 return;
162 }
163
164 setSearching(true);
165 try {
166 const term = query.trim();
167
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`);
170
171 console.log('[Sale Search] Searching for:', term);
172 console.log('[Sale Search] Response status:', response.status);
173
174 if (response.ok) {
175 const data = await response.json();
176 console.log('[Sale Search] Full API response:', data);
177
178 // Handle OpenAPI response structure - can be nested differently based on query
179 let products = data?.data;
180
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;
185 }
186
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;
191 }
192
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');
196 products = [];
197 }
198
199 console.log('[Sale Search] Final products array:', products);
200 console.log('[Sale Search] Products found:', products.length);
201
202 if (Array.isArray(products) && products.length > 0) {
203 console.log('[Sale Search] First product sample:', products[0]);
204 }
205
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) => {
211 switch (searchIn) {
212 case 'name':
213 return (p.Description || p.Name || '').toLowerCase().includes(term.toLowerCase());
214 case 'barcode':
215 return (p.Plu || p.PLU || p.Barcode || '').toLowerCase().includes(term.toLowerCase());
216 case 'pid':
217 return p.Pid === Number(term);
218 default:
219 return true;
220 }
221 });
222 console.log('[Sale Search] Filtered from', originalCount, 'to', filtered.length, 'items');
223 }
224
225 setSearchResults(filtered.slice(0, 20));
226 } else {
227 const errorText = await response.text();
228 console.error('[Sale Search] API error:', response.status, errorText);
229 setSearchResults([]);
230 }
231 } catch (error) {
232 console.error('[Sale Search] Exception:', error);
233 setSearchResults([]);
234 } finally {
235 setSearching(false);
236 }
237 };
238
239 const addProductToSale = (product: any) => {
240 const newItem = {
241 f100: product.Pid, // Product ID
242 f101: 1, // Quantity
243 f102: product.Price || product.SellPrice || '0', // Price
244 f103: product.Description || product.Name || 'Unknown Product' // Description
245 };
246
247 setEditedItems(prev => [...prev, newItem]);
248 setShowAddItem(false);
249 setProductSearch('');
250 setSearchResults([]);
251 };
252
253 const handleSave = async () => {
254 setSaving(true);
255 try {
256 const updatePayload = {
257 ...editData,
258 lineItems: editedItems // Include edited line items
259 };
260
261 const response = await fetch(`/api/v1/sales/${id}`, {
262 method: 'PATCH',
263 headers: { 'Content-Type': 'application/json' },
264 body: JSON.stringify(updatePayload)
265 });
266
267 if (response.ok) {
268 // Update local sale state with edited items
269 setSale(prev => prev ? { ...prev, ...editData, BIG1: editedItems } : null);
270 setEditMode(false);
271 } else {
272 alert('Failed to save changes');
273 }
274 } catch (error) {
275 console.error('Failed to save:', error);
276 alert('Error saving changes');
277 } finally {
278 setSaving(false);
279 }
280 };
281
282 const formatDate = (dateStr: string | undefined) => {
283 if (!dateStr) return '';
284 try {
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');
292
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')}`;
296
297 return `${day}-${monthName}-${year} ${timeStr}`;
298 }
299
300 const date = new Date(dateStr);
301 if (isNaN(date.getTime())) return dateStr;
302
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 });
307
308 return `${day}-${month}-${year} ${time}`;
309 } catch {
310 return dateStr || '';
311 }
312 };
313
314 const getStatusText = (status: number) => {
315 switch (status) {
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}`;
326 }
327 };
328
329 const fieldpineUrl = `https://iig.cwanz.online/report/pos/sales/fieldpine/SingleSale.htm?sid=${id}`;
330
331 if (loading) {
332 return (
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>
337 </div>
338 </div>
339 );
340 }
341
342 if (error) {
343 return (
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>
348 </div>
349 </div>
350 );
351 }
352
353 if (!sale) {
354 return (
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>
358 </div>
359 </div>
360 );
361 }
362
363 return (
364 <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
365 {/* Header */}
366 <div className="mb-6 flex items-center justify-between">
367 <div>
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)})
371 </p>
372 </div>
373 <div className="flex gap-2">
374 {editMode ? (
375 <>
376 <button
377 onClick={() => setEditMode(false)}
378 className="px-4 py-2 bg-surface-2 text-text rounded-md hover:bg-surface-2 transition-colors"
379 disabled={saving}
380 >
381 Cancel
382 </button>
383 <button
384 onClick={handleSave}
385 disabled={saving}
386 className="px-4 py-2 bg-[#00946b] text-white rounded-md hover:bg-[#007a59] transition-colors disabled:opacity-50"
387 >
388 {saving ? 'Saving...' : 'Save Changes'}
389 </button>
390 </>
391 ) : (
392 <button
393 onClick={() => setEditMode(true)}
394 className="px-4 py-2 bg-[#00946b] text-white rounded-md hover:bg-[#007a59] transition-colors"
395 >
396 Edit Sale
397 </button>
398 )}
399 </div>
400 </div>
401
402 {/* Info Blocks */}
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>
407 </div>
408
409 <div className="bg-surface rounded-lg shadow p-4">
410 <div className="text-sm font-medium text-muted">Status</div>
411 {editMode ? (
412 <select
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"
416 >
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>
421 </select>
422 ) : (
423 <div className="mt-1 text-lg font-semibold text-text">{getStatusText(sale.f108)}</div>
424 )}
425 </div>
426
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>
430 </div>
431
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">
435 {sale.f117 !== 0 ? (
436 <a href={`/pages/customers/${sale.f117}`} className="text-[#00946b] hover:underline">
437 {sale.CUST?.[0]?.Name || sale.CUST?.[0]?.f101 || `#${sale.f117}`}
438 </a>
439 ) : (
440 <i>Cash Sale</i>
441 )}
442 </div>
443 </div>
444 </div>
445
446 {/* Editable Fields */}
447 {editMode && (
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">
451 <div>
452 <label className="block text-sm font-medium text-text mb-1">Assigned Store</label>
453 <select
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"
457 >
458 <option value={0}>Not Assigned</option>
459 {locations.map(loc => (
460 <option key={loc.f100} value={loc.f100}>{loc.f101}</option>
461 ))}
462 </select>
463 </div>
464
465 <div>
466 <label className="block text-sm font-medium text-text mb-1">Picking Comments</label>
467 <input
468 type="text"
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"
473 />
474 </div>
475
476 <div className="md:col-span-2">
477 <label className="block text-sm font-medium text-text mb-1">Customer Comments</label>
478 <textarea
479 value={editData.f7123}
480 onChange={(e) => setEditData(prev => ({ ...prev, f7123: e.target.value }))}
481 placeholder="Customer-visible comments..."
482 rows={3}
483 className="block w-full rounded-md border-border shadow-sm focus:border-[#00946b] focus:ring-[#00946b] text-sm"
484 />
485 </div>
486 </div>
487 </div>
488 )}
489
490 {/* External ID & Comments */}
491 {(sale.f130 || sale.f133) && (
492 <div className="bg-surface rounded-lg shadow p-4 mb-6">
493 {sale.f130 && (
494 <div className="mb-2">
495 <span className="font-medium text-text">External ID:</span>{' '}
496 <span className="text-text">{sale.f130}</span>
497 </div>
498 )}
499 {sale.f133 && (
500 <div>
501 <span className="font-medium text-text">Comments:</span>{' '}
502 <span className="text-brand">{sale.f133}</span>
503 </div>
504 )}
505 </div>
506 )}
507
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">
513 <div>
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>
520 )}
521 {sale.DELI[0].f422 && <div>{sale.DELI[0].f422}</div>}
522 </div>
523 </div>
524 <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>
529 </div>
530 )}
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">
536 {sale.DELI[0].f303}
537 </a>
538 </div>
539 </div>
540 )}
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">
546 {sale.DELI[0].f305}
547 </a>
548 </div>
549 </div>
550 )}
551 {sale.DELI[0].f302 && (
552 <div>
553 <div className="text-sm font-medium text-muted">Instructions</div>
554 <div className="mt-1 text-text italic">{sale.DELI[0].f302}</div>
555 </div>
556 )}
557 </div>
558 </div>
559 </div>
560 )}
561
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>
566 {editMode && (
567 <button
568 onClick={() => setShowAddItem(!showAddItem)}
569 className="px-3 py-1 bg-[#00946b] text-white text-sm rounded-md hover:bg-[#007a59] transition-colors"
570 >
571 {showAddItem ? 'Cancel' : '+ Add Item'}
572 </button>
573 )}
574 </div>
575
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>
582 <input
583 type="text"
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"
588 autoFocus
589 />
590 </div>
591 <div className="w-48">
592 <label className="block text-sm font-medium text-text mb-2">Search in</label>
593 <select
594 value={searchIn}
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"
597 >
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>
602 </select>
603 </div>
604 </div>
605 {searching && (
606 <div className="mt-2 text-sm text-muted">Searching...</div>
607 )}
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) => (
612 <button
613 key={product.Pid}
614 onClick={() => addProductToSale(product)}
615 className="w-full px-4 py-3 text-left hover:bg-info/10 transition-colors"
616 >
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}
626 </span>
627 )}
628 </div>
629 </div>
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)}
633 </div>
634 </div>
635 </div>
636 </button>
637 ))}
638 </div>
639 </div>
640 )}
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.
644 </div>
645 )}
646 </div>
647 )}
648
649 <div className="overflow-x-auto">
650 <table className="min-w-full divide-y divide-gray-200">
651 <thead className="bg-surface-2">
652 <tr>
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>}
658 </tr>
659 </thead>
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">
665 <a
666 href={`/pages/products/${item.f100}`}
667 className="text-[#00946b] hover:underline"
668 >
669 {item.f100}
670 </a>
671 </td>
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">
674 {editMode ? (
675 <input
676 type="number"
677 value={item.f101}
678 onChange={(e) => updateLineItem(idx, Number(e.target.value))}
679 min="0"
680 step="1"
681 className="w-20 rounded border-border text-sm focus:border-[#00946b] focus:ring-[#00946b]"
682 />
683 ) : (
684 item.f101
685 )}
686 </td>
687 <td className="px-6 py-4 whitespace-nowrap text-sm text-text text-right">
688 ${parseFloat(item.f102 || '0').toFixed(2)}
689 </td>
690 {editMode && (
691 <td className="px-6 py-4 whitespace-nowrap text-center">
692 <button
693 onClick={() => removeLineItem(idx)}
694 className="text-red-600 hover:text-red-800 text-sm"
695 >
696 Remove
697 </button>
698 </td>
699 )}
700 </tr>
701 ))
702 ) : (
703 <tr>
704 <td colSpan={editMode ? 5 : 4} className="px-6 py-4 text-center text-sm text-muted">
705 No items
706 </td>
707 </tr>
708 )}
709 </tbody>
710 <tfoot className="bg-surface-2">
711 <tr>
712 <td colSpan={3} className="px-6 py-4 text-right text-sm font-semibold text-text">
713 Total:
714 </td>
715 <td className="px-6 py-4 whitespace-nowrap text-sm font-bold text-text text-right">
716 ${sale.f102?.toFixed(2)}
717 </td>
718 </tr>
719 </tfoot>
720 </table>
721 </div>
722 </div>
723
724 {/* Payments */}
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>
728 </div>
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">
734 Awaiting Picking
735 </div>
736 )}
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">
739 Complete
740 </div>
741 )}
742 </div>
743 </div>
744 </div>
745 );
746}