2import { useEffect, useState } from "react";
3import { apiClient } from "@/lib/client/apiClient";
5export default function PurchaseOrdersPage() {
6 const [apiKey, setApiKey] = useState("");
7 const [retailerId, setRetailerId] = useState("");
8 const [purchaseOrders, setPurchaseOrders] = useState<PurchaseOrderHeader[]>([]);
9 const [loading, setLoading] = useState(false);
10 const [selectedPO, setSelectedPO] = useState<PurchaseOrderHeader | null>(null);
11 const [showModal, setShowModal] = useState(false);
12 const [sendViaEmail, setSendViaEmail] = useState(false);
13 const [otherEmail, setOtherEmail] = useState("");
14 const [additionalNotes, setAdditionalNotes] = useState("");
15 const [showEmailOptions, setShowEmailOptions] = useState(false);
18 // Load API credentials from sessionStorage when page mounts
19 const storedApiKey = sessionStorage.getItem("fieldpine_apikey") || "";
20 const storedRetailer = sessionStorage.getItem("fieldpine_retailer") || "";
21 setApiKey(storedApiKey);
22 setRetailerId(storedRetailer);
24 // Load purchase orders
28 const loadPurchaseOrders = async () => {
31 const response = await apiClient.getPurchaseOrders();
32 if (response.success && response.data) {
33 const apiData = response.data as any;
34 const poData = apiData?.data?.PurchaseOrders || [];
35 setPurchaseOrders(poData.map((po: any) => ({
36 f100: Number(po.Poid) || 0,
37 f114: Number(po.State) || 0,
38 f1114: po.StateName || 'Unknown',
39 f1102: po.SupplierName || '',
40 f1105: po.Location || '',
41 f106: po.Created || '',
42 _Sent: po.SentIndicator || '',
43 _Recv: po.ReceivedIndicator || '',
44 f108: po.FirstArrival || '',
45 f104: po.Completed || '',
46 f141: po.SupplierComments || '',
47 f112: po.InternalComments || '',
48 f121: po.SupplierRef || '',
49 f125: Number(po.LineCount) || 0,
50 f126: Number(po.ItemCount) || 0,
51 f130: Number(po.TotalCost) || 0,
52 f131: po.DisplayDate || '',
53 f132: po.AutoSendDate || '',
54 f115: po.SupplierMessage || '',
55 f1203: po.Email || '',
56 f137: po.LastEmailDate || '',
57 f138: po.EmailAcknowledged || '',
62 console.error('Error loading purchase orders:', error);
68 function openPODetails(po: PurchaseOrderHeader) {
73 function closeModal() {
76 setSendViaEmail(false);
78 setAdditionalNotes("");
79 setShowEmailOptions(false);
80 setAdditionalNotes("");
81 setShowEmailOptions(false);
83 // Fieldpine Purchase Order Header fields (no network calls yet)
84 interface PurchaseOrderHeader {
86 f114: number; // State code
87 f1114?: string; // State (text)
88 f1102: string; // Supplier
89 f1105: string; // Ships To
90 f106?: string; // Created
91 f119?: string; // Physical Key
92 _Sent?: string; // Tx indicator
93 _Recv?: string; // Rx indicator
94 f108?: string; // First Arrival
95 f104?: string; // Completed
96 f141?: string; // Comments From Supplier
97 f112?: string; // Internal Comments
98 f121?: string; // Supplier Ref
99 f125?: number; // LineCount
100 f126?: number; // ItemCount
101 f130?: number; // TotalCost
102 f131?: string; // Display Date
103 f132?: string; // Auto Send Date
104 f115?: string; // Comments To Supplier
105 f1203?: string; // Orders Email
106 f137?: string; // Last Email Date
107 f138?: string; // Email Acknowledged Date
108 lines?: PurchaseOrderLine[]; // Line items
111 interface PurchaseOrderLine {
112 f1100: number; // POLineId
113 f01: string; // Product Code
114 f02: string; // Product Description
115 f1107: number; // Qty Ordered
116 f1109: number; // Qty Received
117 f1110: number; // Unit Cost
118 f1111: number; // Extended Cost
119 f1108?: string; // Expected Date
122 const sample: PurchaseOrderHeader[] = [
127 f1102: "Independent Ink Group Pty Ltd",
128 f1105: "Sunbury Agency",
129 f106: "4-Jun-2025 12:17",
143 f1203: "orders@iig.com.au",
145 { f1100: 1, f01: "HP564-BK", f02: "HP 564 Black Ink Cartridge", f1107: 20, f1109: 0, f1110: 18.50, f1111: 370.00, f1108: "10-Jun-2025" },
146 { f1100: 2, f01: "HP564-C", f02: "HP 564 Cyan Ink Cartridge", f1107: 15, f1109: 0, f1110: 16.80, f1111: 252.00, f1108: "10-Jun-2025" },
147 { f1100: 3, f01: "HP564-M", f02: "HP 564 Magenta Ink Cartridge", f1107: 15, f1109: 0, f1110: 16.80, f1111: 252.00, f1108: "10-Jun-2025" },
148 { f1100: 4, f01: "HP564-Y", f02: "HP 564 Yellow Ink Cartridge", f1107: 15, f1109: 0, f1110: 16.80, f1111: 252.00, f1108: "10-Jun-2025" },
149 { f1100: 5, f01: "EP774-BK", f02: "Epson 774 Black Ink Bottle", f1107: 10, f1109: 0, f1110: 22.00, f1111: 220.00, f1108: "10-Jun-2025" },
150 { f1100: 6, f01: "EP774-C", f02: "Epson 774 Cyan Ink Bottle", f1107: 8, f1109: 0, f1110: 19.50, f1111: 156.00, f1108: "10-Jun-2025" },
151 { f1100: 7, f01: "EP774-M", f02: "Epson 774 Magenta Ink Bottle", f1107: 8, f1109: 0, f1110: 19.50, f1111: 156.00, f1108: "10-Jun-2025" },
152 { f1100: 8, f01: "EP774-Y", f02: "Epson 774 Yellow Ink Bottle", f1107: 8, f1109: 0, f1110: 19.50, f1111: 156.00, f1108: "10-Jun-2025" },
159 f1102: "Ausjet Inkjet and Laser Supplies",
160 f1105: "Cowra Agency",
161 f106: "25-Mar-2025 13:38",
175 f1203: "orders@ausjet.com.au",
177 { f1100: 1, f01: "CN045-BK", f02: "Canon 045 Black Toner", f1107: 5, f1109: 0, f1110: 45.00, f1111: 225.00, f1108: "2-Apr-2025" },
178 { f1100: 2, f01: "CN045-C", f02: "Canon 045 Cyan Toner", f1107: 5, f1109: 0, f1110: 42.00, f1111: 210.00, f1108: "2-Apr-2025" },
179 { f1100: 3, f01: "CN045-M", f02: "Canon 045 Magenta Toner", f1107: 5, f1109: 0, f1110: 42.00, f1111: 210.00, f1108: "2-Apr-2025" },
180 { f1100: 4, f01: "CN045-Y", f02: "Canon 045 Yellow Toner", f1107: 5, f1109: 0, f1110: 42.00, f1111: 210.00, f1108: "2-Apr-2025" },
186 { value: -1, label: "Active" },
187 { value: -2, label: "Any" },
188 { value: -3, label: "Any not Finished" },
189 { value: 1, label: "Waiting" },
190 { value: 2, label: "Pre Auth" },
191 { value: 4, label: "Queued" },
192 { value: 17, label: "Rejected" },
193 { value: 18, label: "Cancelled" },
194 { value: -4, label: "Completed" },
197 function handleBulkManage() {
198 // Fieldpine bulk operations logic
199 const action = prompt(
200 `Bulk Manage ${purchaseOrders.length} Purchase Orders\n\n` +
201 'This menu provides advanced options for bulk management.\n' +
202 'Use caution as actions performed are instant and non-reversible\n\n' +
203 'Available operations:\n' +
204 ' 1 = Cancel All (sets f114=18)\n' +
205 ' 2 = Complete All (sets f114=3)\n' +
207 'Enter 1, 2, or cancel:'
210 if (action === '1') {
211 // Bulk Cancel - Fieldpine DATI packet
212 if (!confirm(`Really cancel all ${purchaseOrders.length} purchase orders currently showing?\n\nThis cannot be undone.`)) return;
215 purchaseOrders.forEach((po) => {
216 // In production: POST /gnap/dati with DATI packet
218 f8_s: 'retailmax.elink.purchaseorder.edit',
223 console.log(`Cancelling PO# ${po.f100}`, xPacket);
227 alert(`Requests to cancel ${processed} orders have been sent.\nThe server may take a few minutes to process them all.`);
229 } else if (action === '2') {
230 // Bulk Complete - Fieldpine DATI packet
231 if (!confirm(`Mark all ${purchaseOrders.length} purchase orders as completed?\n\nThis cannot be undone.`)) return;
234 purchaseOrders.forEach((po) => {
236 f8_s: 'retailmax.elink.purchaseorder.edit',
241 console.log(`Completing PO# ${po.f100}`, xPacket);
245 alert(`Requests to complete ${processed} orders have been sent.\nThe server may take a few minutes to process them all.`);
249 function handleUploadDocuments() {
250 // Navigate to document upload queue page
251 window.location.href = '/purchase-orders/upload-document-queue';
254 function handleCreateOrder() {
255 window.location.href = '/purchase-orders/create';
258 function handleDropShipping() {
259 alert('Drop Shipping Order\n\nCreate orders where items are sent directly from supplier to customer:\n\n• Select customer\n• Choose products\n• Add delivery address\n• Set delivery instructions\n\nComing soon!');
260 // window.location.href = '/purchase-orders/create/dropship';
263 function handleViewOrder() {
265 // Generate HTML document and open in new window
266 const html = generateOrderHTML(selectedPO);
267 const newWindow = window.open('', '_blank');
269 newWindow.document.write(html);
270 newWindow.document.close();
275 function handleViewPDF() {
277 // Generate HTML and trigger print dialog (user can save as PDF)
278 const html = generateOrderHTML(selectedPO, true);
279 const printWindow = window.open('', '_blank');
281 printWindow.document.write(html);
282 printWindow.document.close();
283 printWindow.onload = () => {
290 function generateOrderHTML(po: PurchaseOrderHeader, forPrint = false) {
295 <meta charset="UTF-8">
296 <title>Purchase Order #${po.f100}</title>
298 body { font-family: Arial, sans-serif; margin: 40px; color: #333; }
299 .header { border-bottom: 3px solid #00483d; padding-bottom: 20px; margin-bottom: 30px; }
300 .header h1 { color: #00483d; margin: 0; }
301 .header .po-number { font-size: 24px; color: #666; }
302 .info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 30px; }
303 .info-item { margin-bottom: 10px; }
304 .info-label { font-weight: bold; color: #666; font-size: 14px; }
305 .info-value { font-size: 16px; margin-top: 5px; }
306 table { width: 100%; border-collapse: collapse; margin: 20px 0; }
307 thead { background-color: #00483d; color: white; }
308 th { padding: 12px; text-align: left; font-weight: 600; }
309 td { padding: 10px; border-bottom: 1px solid #e6eef6; }
310 tbody tr:hover { background-color: #f8f9fa; }
311 .totals { margin-top: 30px; text-align: right; }
312 .totals-row { display: flex; justify-content: flex-end; gap: 20px; margin: 10px 0; }
313 .totals-label { font-weight: bold; min-width: 150px; }
314 .comments { margin-top: 30px; }
315 .comments-section { margin: 20px 0; padding: 15px; background: #f8f9fa; border-radius: 8px; }
316 .comments-title { font-weight: bold; margin-bottom: 10px; color: #00483d; }
317 .text-right { text-align: right; }
318 .text-center { text-align: center; }
319 ${forPrint ? '@media print { body { margin: 20px; } .no-print { display: none; } }' : ''}
324 <h1>Purchase Order</h1>
325 <div class="po-number">PO# ${po.f100}</div>
328 <div class="info-grid">
330 <div class="info-item">
331 <div class="info-label">Supplier</div>
332 <div class="info-value">${po.f1102}</div>
334 <div class="info-item">
335 <div class="info-label">Ships To</div>
336 <div class="info-value">${po.f1105}</div>
340 <div class="info-item">
341 <div class="info-label">Created</div>
342 <div class="info-value">${po.f106}</div>
344 <div class="info-item">
345 <div class="info-label">State</div>
346 <div class="info-value">${po.f1114}</div>
351 <h2 style="color: #00483d; margin-top: 40px;">Line Items</h2>
355 <th>Product Code</th>
357 <th class="text-center">Qty Ordered</th>
358 <th class="text-right">Unit Cost</th>
359 <th class="text-right">Extended Cost</th>
360 <th>Expected Date</th>
364 ${po.lines?.map(line => `
366 <td style="font-family: monospace; color: #00483d;">${line.f01}</td>
368 <td class="text-center">${line.f1107}</td>
369 <td class="text-right">$${line.f1110.toFixed(2)}</td>
370 <td class="text-right" style="font-weight: 600;">$${line.f1111.toFixed(2)}</td>
371 <td>${line.f1108 || '—'}</td>
378 <div class="totals-row">
379 <div class="totals-label">Line Count:</div>
380 <div>${po.f125}</div>
382 <div class="totals-row">
383 <div class="totals-label">Item Count:</div>
384 <div>${po.f126}</div>
386 <div class="totals-row" style="font-size: 18px; font-weight: bold; color: #00483d; margin-top: 10px;">
387 <div class="totals-label">Total Cost:</div>
388 <div>$${po.f130?.toLocaleString()}</div>
392 ${(po.f115 || po.f112) ? `
393 <div class="comments">
395 <div class="comments-section">
396 <div class="comments-title">Supplier Comments</div>
397 <div>${po.f115}</div>
401 <div class="comments-section">
402 <div class="comments-title">Internal Comments</div>
403 <div>${po.f112}</div>
409 ${forPrint ? '' : '<div class="no-print" style="margin-top: 40px; text-align: center;"><button onclick="window.print()" style="padding: 10px 20px; background: #00483d; color: white; border: none; border-radius: 5px; cursor: pointer;">Print / Save as PDF</button></div>'}
415 function handleCopyClipboard() {
417 // Format similar to Fieldpine's meetu4po format
419 `Purchase Order #${selectedPO.f100}`,
420 `Supplier: ${selectedPO.f1102}`,
421 `Ships To: ${selectedPO.f1105}`,
422 `Created: ${selectedPO.f106}`,
423 `State: ${selectedPO.f1114}`,
424 `Line Count: ${selectedPO.f125}`,
425 `Item Count: ${selectedPO.f126}`,
426 `Total Cost: $${selectedPO.f130?.toLocaleString()}`,
431 if (selectedPO.lines) {
432 selectedPO.lines.forEach(line => {
433 lines.push(` ${line.f01} - ${line.f02} - Qty: ${line.f1107} @ $${line.f1110.toFixed(2)} = $${line.f1111.toFixed(2)}`);
437 const text = lines.join('\n');
439 if (navigator.clipboard) {
440 navigator.clipboard.writeText(text).then(() => {
441 // Visual feedback - briefly change button text
442 const btn = document.activeElement as HTMLButtonElement;
443 if (btn && btn.textContent) {
444 const originalText = btn.textContent;
445 btn.textContent = '✓ Copied!';
446 btn.style.backgroundColor = '#10b981';
448 btn.textContent = originalText;
449 btn.style.backgroundColor = '';
454 const ta = document.createElement("textarea");
456 ta.style.position = 'fixed';
457 ta.style.opacity = '0';
458 document.body.appendChild(ta);
461 document.execCommand('copy');
462 alert("✓ Copied to clipboard!");
464 alert("✗ Failed to copy to clipboard");
466 document.body.removeChild(ta);
472 function handleReceive() {
473 if (!selectedPO) return;
475 // Fieldpine logic: Direct navigation without prompt (as per original code)
476 let url = `/purchase-orders/receive?poid=${selectedPO.f100}`;
477 if (selectedPO.f119 && selectedPO.f119.length > 8) {
478 url += `&physkey=${selectedPO.f119}`;
481 // In production: window.location.href = url;
482 alert(`Opening receive page for PO# ${selectedPO.f100}\n\nURL: ${url}\n\nNote: Receive page navigation is immediate (no confirmation prompt as per Fieldpine standard)`);
483 console.log('Navigate to receive page:', url);
486 function handleCancelPO() {
487 if (!selectedPO) return;
489 if (confirm(`Really cancel this purchase order?\n\nPO# ${selectedPO.f100} - ${selectedPO.f1102}`)) {
490 // Fieldpine DATI packet structure
493 f8_s: 'retailmax.elink.purchaseorder.edit',
495 f100_E: selectedPO.f100,
500 // API call: POST to /gnap/dati
501 console.log('Cancel PO DATI packet:', xPacket);
503 // Update local state
504 selectedPO.f114 = 18;
505 selectedPO.f1114 = 'Cancelled';
507 // Remove from grid and re-render
508 setPurchaseOrders(prev => prev.filter(po => po.f100 !== selectedPO.f100));
510 alert('Purchase order has been cancelled');
515 function handleSendEmail() {
516 if (!selectedPO) return;
518 const em = sendViaEmail;
519 const edi = false; // Would come from checkbox
520 const manual = false;
522 if (!em && !edi && !manual) {
523 alert('Please select at least one method of sending the order to the supplier');
528 const targetEmail = otherEmail.trim() || selectedPO.f1203;
530 alert('No email address available for this supplier');
534 // Fieldpine paramsyms structure
536 addnotes: additionalNotes
539 // In production: POST /GNAP/DATI with:
541 f8_s: 'retailmax.elink.purchaseorder.action',
543 f100_E: selectedPO.f100,
544 f101_E: '1', // email to supplier
545 f110_s: otherEmail || undefined, // override email if specified
546 f300_s: JSON.stringify(paramsyms) // additional parameters
549 console.log('Email PO API packet:', apiPacket);
551 // Create mailto link as fallback
552 const subject = encodeURIComponent(`Purchase Order #${selectedPO.f100} from ${selectedPO.f1105}`);
554 // Create professional email body
555 let body = `Dear ${selectedPO.f1102},\n\n`;
556 body += `Please find our purchase order details below:\n\n`;
557 body += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
558 body += `PURCHASE ORDER #${selectedPO.f100}\n`;
559 body += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n`;
561 body += `Store: ${selectedPO.f1105}\n`;
562 body += `Order Date: ${selectedPO.f106}\n`;
563 body += `Status: ${selectedPO.f1114}\n\n`;
565 if (additionalNotes) {
566 body += `ADDITIONAL NOTES:\n${additionalNotes}\n\n`;
569 body += `ORDER SUMMARY:\n`;
570 body += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
571 body += `Total Lines: ${selectedPO.f125}\n`;
572 body += `Total Items: ${selectedPO.f126}\n`;
573 body += `Total Cost: $${selectedPO.f130?.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}\n\n`;
575 body += `LINE ITEMS:\n`;
576 body += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
577 selectedPO.lines?.forEach((line, index) => {
578 body += `${index + 1}. ${line.f01}\n`;
579 body += ` ${line.f02}\n`;
580 body += ` Quantity: ${line.f1107} @ $${line.f1110.toFixed(2)} each\n`;
581 body += ` Line Total: $${line.f1111.toFixed(2)}\n`;
582 if (line.f1108) body += ` Expected: ${line.f1108}\n`;
586 if (selectedPO.f115) {
587 body += `\nSUPPLIER COMMENTS:\n${selectedPO.f115}\n\n`;
590 body += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
591 body += `\nThank you for your service.\n\n`;
592 body += `Best regards,\n${selectedPO.f1105}\n\n`;
593 body += `---\nThis purchase order was generated by EverydayPOS\n`;
594 body += `PO Reference: ${selectedPO.f100}`;
596 const mailtoLink = `mailto:${targetEmail}?subject=${subject}&body=${encodeURIComponent(body)}`;
597 window.location.href = mailtoLink;
599 // Show success message
600 alert(`✓ Email client opened\n\nPO #${selectedPO.f100} ready to send to ${targetEmail}\n\nIn production, this will be sent automatically via the Fieldpine API.`);
602 // Mark as sent with current timestamp
603 const now = new Date().toISOString();
604 selectedPO.f137 = now;
606 // Update PO state to "Waiting" if it was "Pre-Auth"
607 if (selectedPO.f114 === 2) {
609 selectedPO.f1114 = "Waiting";
612 // Update in purchaseOrders array
613 setPurchaseOrders(prev => prev.map(po => {
614 if (po.f100 === selectedPO.f100) {
618 f114: selectedPO.f114 === 2 ? 1 : po.f114,
619 f1114: selectedPO.f114 === 2 ? "Waiting" : po.f1114
630 <div className="min-h-screen bg-bg p-4 md:p-6">
631 <div className="max-w-7xl mx-auto">
632 <div className="mb-6">
633 <h1 className="text-3xl font-bold text-brand mb-2">Purchase Orders</h1>
634 <p className="text-muted">Manage and view supplier purchase orders</p>
637 <div className="card mb-6">
638 <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
640 <label className="block text-sm font-medium text-text mb-2">From (including)</label>
641 <input type="date" className="w-full px-4 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand" />
644 <label className="block text-sm font-medium text-text mb-2">To (excluding)</label>
645 <input type="date" className="w-full px-4 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand" />
648 <label className="block text-sm font-medium text-text mb-2">State</label>
649 <select className="w-full px-4 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand">
651 <option key={s.value} value={s.value}>{s.label}</option>
656 <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-4">
658 <label className="block text-sm font-medium text-text mb-2">Search</label>
659 <input type="text" placeholder="keywords" className="w-full px-4 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand" />
661 <div className="md:col-span-2">
662 <label className="block text-sm font-medium text-text mb-2">Supplier</label>
663 <input type="text" placeholder="supplier name" className="w-full px-4 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand" />
667 {/* Action Buttons */}
668 <div className="mt-6 pt-6 border-t border-border flex flex-wrap gap-3">
669 <button onClick={handleBulkManage} className="px-4 py-2 text-white rounded-lg hover:opacity-80 text-sm font-medium cursor-pointer transition-all bg-brand">
672 <button onClick={handleUploadDocuments} className="px-4 py-2 text-white rounded-lg hover:opacity-80 text-sm font-medium cursor-pointer transition-all bg-brand">
675 <button onClick={handleCreateOrder} className="px-4 py-2 text-white rounded-lg hover:opacity-80 text-sm font-medium cursor-pointer transition-all shadow-sm font-semibold bg-brand">
678 <button onClick={handleDropShipping} className="px-4 py-2 text-white rounded-lg hover:opacity-80 text-sm font-medium cursor-pointer transition-all bg-brand">
684 <div className="card overflow-x-auto">
685 <table className="w-full text-sm">
687 <tr className="border-b border-border">
688 <th className="text-left py-3 px-4 font-semibold text-text">Poid#</th>
689 <th className="text-left py-3 px-4 font-semibold text-text">State</th>
690 <th className="text-left py-3 px-4 font-semibold text-text">Supplier</th>
691 <th className="text-left py-3 px-4 font-semibold text-text">Ships To</th>
692 <th className="text-left py-3 px-4 font-semibold text-text">Created</th>
693 <th className="text-left py-3 px-4 font-semibold text-text">Tx</th>
694 <th className="text-left py-3 px-4 font-semibold text-text">Rx</th>
695 <th className="text-left py-3 px-4 font-semibold text-text">First Arrival</th>
696 <th className="text-left py-3 px-4 font-semibold text-text">Completed</th>
697 <th className="text-left py-3 px-4 font-semibold text-text">Supplier Ref</th>
698 <th className="text-right py-3 px-4 font-semibold text-text">Total Cost</th>
699 <th className="text-left py-3 px-4 font-semibold text-text">Display Date</th>
700 <th className="text-left py-3 px-4 font-semibold text-text">Auto Send Date</th>
701 <th className="text-right py-3 px-4 font-semibold text-text">LineCount</th>
702 <th className="text-right py-3 px-4 font-semibold text-text">ItemCount</th>
703 <th className="text-left py-3 px-4 font-semibold text-text">Comments From Supplier</th>
704 <th className="text-left py-3 px-4 font-semibold text-text">Comments To Supplier</th>
705 <th className="text-left py-3 px-4 font-semibold text-text">Internal Comments</th>
706 <th className="text-left py-3 px-4 font-semibold text-text">Orders Email</th>
712 <td colSpan={19} className="py-8 text-center text-muted">
713 <div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-brand"></div>
714 <p className="mt-2">Loading purchase orders...</p>
717 ) : purchaseOrders.length === 0 ? (
719 <td colSpan={19} className="py-8 text-center text-muted">
720 No purchase orders found
723 ) : purchaseOrders.map((row, idx) => (
726 onClick={() => openPODetails(row)}
727 className={`border-b border-border hover:bg-surface-2 transition-colors cursor-pointer ${
728 idx % 2 === 0 ? "bg-white" : "bg-gray-50"
731 <td className="py-3 px-4 text-brand font-medium">{row.f100}</td>
732 <td className="py-3 px-4">{row.f1114}</td>
733 <td className="py-3 px-4">{row.f1102}</td>
734 <td className="py-3 px-4">{row.f1105}</td>
735 <td className="py-3 px-4">{row.f106}</td>
736 <td className="py-3 px-4">{row._Sent}</td>
737 <td className="py-3 px-4">{row._Recv}</td>
738 <td className="py-3 px-4">{row.f108}</td>
739 <td className="py-3 px-4">{row.f104}</td>
740 <td className="py-3 px-4">{row.f121}</td>
741 <td className="py-3 px-4 text-right">{row.f130?.toLocaleString(undefined, { style: "currency", currency: "USD" }).replace("USD", "$")}</td>
742 <td className="py-3 px-4">{row.f131}</td>
743 <td className="py-3 px-4">{row.f132}</td>
744 <td className="py-3 px-4 text-right">{row.f125}</td>
745 <td className="py-3 px-4 text-right">{row.f126}</td>
746 <td className="py-3 px-4">{row.f141}</td>
747 <td className="py-3 px-4">{row.f115}</td>
748 <td className="py-3 px-4">{row.f112}</td>
749 <td className="py-3 px-4">{row.f1203}</td>
756 {/* Purchase Order Detail Modal */}
757 {showModal && selectedPO && (
758 <div className="fixed inset-0 bg-black/30 z-50 flex items-start justify-center p-4 overflow-y-auto" onClick={closeModal}>
759 <div className="bg-white rounded-lg shadow-xl max-w-4xl w-full my-8" onClick={(e) => e.stopPropagation()}>
760 <div className="border-b border-border p-4 flex items-center justify-between">
761 <h2 className="text-xl font-bold text-brand">{selectedPO.f1102} <span className="text-sm text-muted">(PO# {selectedPO.f100})</span></h2>
762 <button onClick={closeModal} className="px-3 py-1 text-sm border border-border rounded hover:bg-surface cursor-pointer">Close</button>
765 <div className="p-6 space-y-6">
767 <div className="flex flex-wrap gap-2 pb-4 border-b border-border">
768 <button onClick={handleViewOrder} className="px-3 py-2 text-sm border border-border rounded-lg hover:bg-surface cursor-pointer">View Order</button>
769 <button onClick={handleViewPDF} className="px-3 py-2 text-sm border border-border rounded-lg hover:bg-surface cursor-pointer">View PDF</button>
770 <button onClick={handleCopyClipboard} className="px-3 py-2 text-sm border border-border rounded-lg hover:bg-surface cursor-pointer">Copy Clipboard</button>
771 <button onClick={handleReceive} className="px-3 py-2 text-sm bg-success text-white rounded-lg hover:opacity-90 cursor-pointer">Receive</button>
772 <button onClick={handleCancelPO} className="px-3 py-2 text-sm bg-danger text-white rounded-lg hover:opacity-90 cursor-pointer">Cancel</button>
774 {/* Email Sending Section */}
775 <div className="border border-border rounded-lg p-4 bg-gray-50">
776 <div className="flex items-center justify-between mb-3">
777 <label className="flex items-center gap-2 cursor-pointer">
780 checked={sendViaEmail}
781 onChange={(e) => setSendViaEmail(e.target.checked)}
782 disabled={!selectedPO.f1203 && !otherEmail}
783 className="w-4 h-4 text-brand rounded focus:ring-2 focus:ring-brand"
785 <span className="font-medium">Email To Supplier</span>
787 {selectedPO.f1203 && (
789 onClick={() => setShowEmailOptions(!showEmailOptions)}
790 className="text-sm text-muted hover:text-brand cursor-pointer"
792 {showEmailOptions ? '▲' : '▼'} Options
797 {selectedPO.f1203 && (
798 <div className="text-sm text-muted mb-3">
799 Default: {selectedPO.f1203}
803 {showEmailOptions && (
804 <div className="space-y-3 mb-3">
806 <label className="block text-sm font-medium text-text mb-1">
807 Other Email (override)
813 setOtherEmail(e.target.value);
814 if (e.target.value.length > 3) {
815 setSendViaEmail(true);
818 placeholder="Enter alternative email address"
819 className="w-full px-3 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand text-sm"
824 <label className="block text-sm font-medium text-text mb-1">
828 value={additionalNotes}
829 onChange={(e) => setAdditionalNotes(e.target.value)}
831 placeholder="Enter any extra message to be added to the email"
832 className="w-full px-3 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand text-sm"
834 <p className="text-xs text-muted mt-1 italic">
835 Additional notes require the template to support showing them
842 onClick={handleSendEmail}
843 disabled={!sendViaEmail}
844 className="w-full px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 disabled:bg-gray-300 disabled:cursor-not-allowed cursor-pointer font-medium"
851 <div className="grid grid-cols-2 gap-4">
853 <p className="text-sm text-muted mb-1">Ships To</p>
854 <p className="font-medium">{selectedPO.f1105}</p>
857 <p className="text-sm text-muted mb-1">Created</p>
858 <p className="font-medium">{selectedPO.f106}</p>
861 <p className="text-sm text-muted mb-1">State</p>
862 <p className="font-medium">{selectedPO.f1114}</p>
865 <p className="text-sm text-muted mb-1">Email Status</p>
866 <p className="font-medium">
868 <span className="text-green-600 font-semibold">✓ Sent & Confirmed</span>
869 ) : selectedPO.f137 ? (
870 <span className="text-blue-600 font-semibold">✓ Sent</span>
871 ) : selectedPO.f132 ? (
872 <span className="text-orange-600">⏰ Queued</span>
874 <span className="text-gray-500">Not Sent</span>
879 <p className="text-sm text-muted mb-1">Supplier Ref</p>
880 <p className="font-medium">{selectedPO.f121 || "—"}</p>
882 {selectedPO.f137 && (
884 <p className="text-sm text-muted mb-1">Last Emailed</p>
885 <p className="font-medium text-sm">{new Date(selectedPO.f137).toLocaleString()}</p>
891 <div className="flex gap-6 text-sm">
892 <div className="px-4 py-2 bg-gray-50 rounded">
893 <span className="text-muted">Line Count: </span>
894 <span className="font-semibold">{selectedPO.f125}</span>
896 <div className="px-4 py-2 bg-gray-50 rounded">
897 <span className="text-muted">Item Count: </span>
898 <span className="font-semibold">{selectedPO.f126}</span>
900 <div className="px-4 py-2 bg-gray-50 rounded">
901 <span className="text-muted">Total Cost: </span>
902 <span className="font-semibold">${selectedPO.f130?.toLocaleString()}</span>
906 {/* Line Items Table */}
908 <h3 className="text-sm font-semibold mb-2">Line Items</h3>
909 <div className="border border-border rounded overflow-hidden">
910 <table className="w-full text-sm">
911 <thead className="bg-gray-50 border-b border-border">
913 <th className="text-left px-3 py-2 font-medium text-text">Product Code</th>
914 <th className="text-left px-3 py-2 font-medium text-text">Description</th>
915 <th className="text-right px-3 py-2 font-medium text-text">Qty Ordered</th>
916 <th className="text-right px-3 py-2 font-medium text-text">Qty Received</th>
917 <th className="text-right px-3 py-2 font-medium text-text">Unit Cost</th>
918 <th className="text-right px-3 py-2 font-medium text-text">Extended Cost</th>
919 <th className="text-left px-3 py-2 font-medium text-text">Expected Date</th>
923 {selectedPO.lines && selectedPO.lines.length > 0 ? (
924 selectedPO.lines.map((line) => (
925 <tr key={line.f1100} className="border-b border-border hover:bg-gray-50">
926 <td className="px-3 py-2 font-mono text-brand">{line.f01}</td>
927 <td className="px-3 py-2">{line.f02}</td>
928 <td className="px-3 py-2 text-right">{line.f1107}</td>
929 <td className="px-3 py-2 text-right">{line.f1109}</td>
930 <td className="px-3 py-2 text-right">${line.f1110.toFixed(2)}</td>
931 <td className="px-3 py-2 text-right font-semibold">${line.f1111.toFixed(2)}</td>
932 <td className="px-3 py-2">{line.f1108 || "—"}</td>
937 <td colSpan={7} className="px-3 py-4 text-center text-muted">No line items</td>
946 <div className="space-y-4">
948 <label className="block text-sm font-medium text-text mb-2">Supplier Comments</label>
951 className="w-full px-3 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand"
952 defaultValue={selectedPO.f115}
956 <label className="block text-sm font-medium text-text mb-2">Internal Comments</label>
959 className="w-full px-3 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand"
960 defaultValue={selectedPO.f112}