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 } from "react";
3import { apiClient } from "@/lib/client/apiClient";
4
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);
16
17 useEffect(() => {
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);
23
24 // Load purchase orders
25 loadPurchaseOrders();
26 }, []);
27
28 const loadPurchaseOrders = async () => {
29 setLoading(true);
30 try {
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 || '',
58 lines: po.Lines || []
59 })));
60 }
61 } catch (error) {
62 console.error('Error loading purchase orders:', error);
63 } finally {
64 setLoading(false);
65 }
66 };
67
68 function openPODetails(po: PurchaseOrderHeader) {
69 setSelectedPO(po);
70 setShowModal(true);
71 }
72
73 function closeModal() {
74 setShowModal(false);
75 setSelectedPO(null);
76 setSendViaEmail(false);
77 setOtherEmail("");
78 setAdditionalNotes("");
79 setShowEmailOptions(false);
80 setAdditionalNotes("");
81 setShowEmailOptions(false);
82 }
83 // Fieldpine Purchase Order Header fields (no network calls yet)
84 interface PurchaseOrderHeader {
85 f100: number; // Poid
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
109 }
110
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
120 }
121
122 const sample: PurchaseOrderHeader[] = [
123 {
124 f100: 4,
125 f114: 2,
126 f1114: "Pre-Auth",
127 f1102: "Independent Ink Group Pty Ltd",
128 f1105: "Sunbury Agency",
129 f106: "4-Jun-2025 12:17",
130 _Sent: ".",
131 _Recv: "",
132 f108: "",
133 f104: "",
134 f141: "",
135 f112: "",
136 f121: "",
137 f125: 12,
138 f126: 113,
139 f130: 2038,
140 f131: "",
141 f132: "",
142 f115: "",
143 f1203: "orders@iig.com.au",
144 lines: [
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" },
153 ],
154 },
155 {
156 f100: 2,
157 f114: 2,
158 f1114: "Pre-Auth",
159 f1102: "Ausjet Inkjet and Laser Supplies",
160 f1105: "Cowra Agency",
161 f106: "25-Mar-2025 13:38",
162 _Sent: ".",
163 _Recv: "",
164 f108: "",
165 f104: "",
166 f141: "",
167 f112: "",
168 f121: "",
169 f125: 8,
170 f126: 45,
171 f130: 880,
172 f131: "",
173 f132: "",
174 f115: "",
175 f1203: "orders@ausjet.com.au",
176 lines: [
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" },
181 ],
182 },
183 ];
184
185 const states = [
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" },
195 ];
196
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' +
206 '\n' +
207 'Enter 1, 2, or cancel:'
208 );
209
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;
213
214 let processed = 0;
215 purchaseOrders.forEach((po) => {
216 // In production: POST /gnap/dati with DATI packet
217 const xPacket = {
218 f8_s: 'retailmax.elink.purchaseorder.edit',
219 f11_B: 'I',
220 f100_E: po.f100,
221 f114_E: '18'
222 };
223 console.log(`Cancelling PO# ${po.f100}`, xPacket);
224 processed++;
225 });
226
227 alert(`Requests to cancel ${processed} orders have been sent.\nThe server may take a few minutes to process them all.`);
228
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;
232
233 let processed = 0;
234 purchaseOrders.forEach((po) => {
235 const xPacket = {
236 f8_s: 'retailmax.elink.purchaseorder.edit',
237 f11_B: 'I',
238 f100_E: po.f100,
239 f114_E: '3'
240 };
241 console.log(`Completing PO# ${po.f100}`, xPacket);
242 processed++;
243 });
244
245 alert(`Requests to complete ${processed} orders have been sent.\nThe server may take a few minutes to process them all.`);
246 }
247 }
248
249 function handleUploadDocuments() {
250 // Navigate to document upload queue page
251 window.location.href = '/purchase-orders/upload-document-queue';
252 }
253
254 function handleCreateOrder() {
255 window.location.href = '/purchase-orders/create';
256 }
257
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';
261 }
262
263 function handleViewOrder() {
264 if (selectedPO) {
265 // Generate HTML document and open in new window
266 const html = generateOrderHTML(selectedPO);
267 const newWindow = window.open('', '_blank');
268 if (newWindow) {
269 newWindow.document.write(html);
270 newWindow.document.close();
271 }
272 }
273 }
274
275 function handleViewPDF() {
276 if (selectedPO) {
277 // Generate HTML and trigger print dialog (user can save as PDF)
278 const html = generateOrderHTML(selectedPO, true);
279 const printWindow = window.open('', '_blank');
280 if (printWindow) {
281 printWindow.document.write(html);
282 printWindow.document.close();
283 printWindow.onload = () => {
284 printWindow.print();
285 };
286 }
287 }
288 }
289
290 function generateOrderHTML(po: PurchaseOrderHeader, forPrint = false) {
291 return `
292 <!DOCTYPE html>
293 <html>
294 <head>
295 <meta charset="UTF-8">
296 <title>Purchase Order #${po.f100}</title>
297 <style>
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; } }' : ''}
320 </style>
321 </head>
322 <body>
323 <div class="header">
324 <h1>Purchase Order</h1>
325 <div class="po-number">PO# ${po.f100}</div>
326 </div>
327
328 <div class="info-grid">
329 <div>
330 <div class="info-item">
331 <div class="info-label">Supplier</div>
332 <div class="info-value">${po.f1102}</div>
333 </div>
334 <div class="info-item">
335 <div class="info-label">Ships To</div>
336 <div class="info-value">${po.f1105}</div>
337 </div>
338 </div>
339 <div>
340 <div class="info-item">
341 <div class="info-label">Created</div>
342 <div class="info-value">${po.f106}</div>
343 </div>
344 <div class="info-item">
345 <div class="info-label">State</div>
346 <div class="info-value">${po.f1114}</div>
347 </div>
348 </div>
349 </div>
350
351 <h2 style="color: #00483d; margin-top: 40px;">Line Items</h2>
352 <table>
353 <thead>
354 <tr>
355 <th>Product Code</th>
356 <th>Description</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>
361 </tr>
362 </thead>
363 <tbody>
364 ${po.lines?.map(line => `
365 <tr>
366 <td style="font-family: monospace; color: #00483d;">${line.f01}</td>
367 <td>${line.f02}</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>
372 </tr>
373 `).join('')}
374 </tbody>
375 </table>
376
377 <div class="totals">
378 <div class="totals-row">
379 <div class="totals-label">Line Count:</div>
380 <div>${po.f125}</div>
381 </div>
382 <div class="totals-row">
383 <div class="totals-label">Item Count:</div>
384 <div>${po.f126}</div>
385 </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>
389 </div>
390 </div>
391
392 ${(po.f115 || po.f112) ? `
393 <div class="comments">
394 ${po.f115 ? `
395 <div class="comments-section">
396 <div class="comments-title">Supplier Comments</div>
397 <div>${po.f115}</div>
398 </div>
399 ` : ''}
400 ${po.f112 ? `
401 <div class="comments-section">
402 <div class="comments-title">Internal Comments</div>
403 <div>${po.f112}</div>
404 </div>
405 ` : ''}
406 </div>
407 ` : ''}
408
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>'}
410 </body>
411 </html>
412 `;
413 }
414
415 function handleCopyClipboard() {
416 if (selectedPO) {
417 // Format similar to Fieldpine's meetu4po format
418 const lines = [
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()}`,
427 ``,
428 `Line Items:`,
429 ];
430
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)}`);
434 });
435 }
436
437 const text = lines.join('\n');
438
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';
447 setTimeout(() => {
448 btn.textContent = originalText;
449 btn.style.backgroundColor = '';
450 }, 2000);
451 }
452 }).catch(() => {
453 // Fallback method
454 const ta = document.createElement("textarea");
455 ta.value = text;
456 ta.style.position = 'fixed';
457 ta.style.opacity = '0';
458 document.body.appendChild(ta);
459 ta.select();
460 try {
461 document.execCommand('copy');
462 alert("✓ Copied to clipboard!");
463 } catch (err) {
464 alert("✗ Failed to copy to clipboard");
465 }
466 document.body.removeChild(ta);
467 });
468 }
469 }
470 }
471
472 function handleReceive() {
473 if (!selectedPO) return;
474
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}`;
479 }
480
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);
484 }
485
486 function handleCancelPO() {
487 if (!selectedPO) return;
488
489 if (confirm(`Really cancel this purchase order?\n\nPO# ${selectedPO.f100} - ${selectedPO.f1102}`)) {
490 // Fieldpine DATI packet structure
491 const xPacket = {
492 DATI: {
493 f8_s: 'retailmax.elink.purchaseorder.edit',
494 f11_B: 'I',
495 f100_E: selectedPO.f100,
496 f114_E: '18'
497 }
498 };
499
500 // API call: POST to /gnap/dati
501 console.log('Cancel PO DATI packet:', xPacket);
502
503 // Update local state
504 selectedPO.f114 = 18;
505 selectedPO.f1114 = 'Cancelled';
506
507 // Remove from grid and re-render
508 setPurchaseOrders(prev => prev.filter(po => po.f100 !== selectedPO.f100));
509
510 alert('Purchase order has been cancelled');
511 closeModal();
512 }
513 }
514
515 function handleSendEmail() {
516 if (!selectedPO) return;
517
518 const em = sendViaEmail;
519 const edi = false; // Would come from checkbox
520 const manual = false;
521
522 if (!em && !edi && !manual) {
523 alert('Please select at least one method of sending the order to the supplier');
524 return;
525 }
526
527 if (em) {
528 const targetEmail = otherEmail.trim() || selectedPO.f1203;
529 if (!targetEmail) {
530 alert('No email address available for this supplier');
531 return;
532 }
533
534 // Fieldpine paramsyms structure
535 const paramsyms = {
536 addnotes: additionalNotes
537 };
538
539 // In production: POST /GNAP/DATI with:
540 const apiPacket = {
541 f8_s: 'retailmax.elink.purchaseorder.action',
542 f11_B: 'I',
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
547 };
548
549 console.log('Email PO API packet:', apiPacket);
550
551 // Create mailto link as fallback
552 const subject = encodeURIComponent(`Purchase Order #${selectedPO.f100} from ${selectedPO.f1105}`);
553
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`;
560
561 body += `Store: ${selectedPO.f1105}\n`;
562 body += `Order Date: ${selectedPO.f106}\n`;
563 body += `Status: ${selectedPO.f1114}\n\n`;
564
565 if (additionalNotes) {
566 body += `ADDITIONAL NOTES:\n${additionalNotes}\n\n`;
567 }
568
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`;
574
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`;
583 body += `\n`;
584 });
585
586 if (selectedPO.f115) {
587 body += `\nSUPPLIER COMMENTS:\n${selectedPO.f115}\n\n`;
588 }
589
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}`;
595
596 const mailtoLink = `mailto:${targetEmail}?subject=${subject}&body=${encodeURIComponent(body)}`;
597 window.location.href = mailtoLink;
598
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.`);
601
602 // Mark as sent with current timestamp
603 const now = new Date().toISOString();
604 selectedPO.f137 = now;
605
606 // Update PO state to "Waiting" if it was "Pre-Auth"
607 if (selectedPO.f114 === 2) {
608 selectedPO.f114 = 1;
609 selectedPO.f1114 = "Waiting";
610 }
611
612 // Update in purchaseOrders array
613 setPurchaseOrders(prev => prev.map(po => {
614 if (po.f100 === selectedPO.f100) {
615 return {
616 ...po,
617 f137: now,
618 f114: selectedPO.f114 === 2 ? 1 : po.f114,
619 f1114: selectedPO.f114 === 2 ? "Waiting" : po.f1114
620 };
621 }
622 return po;
623 }));
624
625 closeModal();
626 }
627 }
628
629 return (
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>
635 </div>
636
637 <div className="card mb-6">
638 <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
639 <div>
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" />
642 </div>
643 <div>
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" />
646 </div>
647 <div>
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">
650 {states.map((s) => (
651 <option key={s.value} value={s.value}>{s.label}</option>
652 ))}
653 </select>
654 </div>
655 </div>
656 <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-4">
657 <div>
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" />
660 </div>
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" />
664 </div>
665 </div>
666
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">
670 Bulk Manage
671 </button>
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">
673 Upload Documents
674 </button>
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">
676 Create New Order
677 </button>
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">
679 Drop Shipping Order
680 </button>
681 </div>
682 </div>
683
684 <div className="card overflow-x-auto">
685 <table className="w-full text-sm">
686 <thead>
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>
707 </tr>
708 </thead>
709 <tbody>
710 {loading ? (
711 <tr>
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>
715 </td>
716 </tr>
717 ) : purchaseOrders.length === 0 ? (
718 <tr>
719 <td colSpan={19} className="py-8 text-center text-muted">
720 No purchase orders found
721 </td>
722 </tr>
723 ) : purchaseOrders.map((row, idx) => (
724 <tr
725 key={row.f100}
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"
729 }`}
730 >
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>
750 </tr>
751 ))}
752 </tbody>
753 </table>
754 </div>
755
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>
763 </div>
764
765 <div className="p-6 space-y-6">
766 {/* Actions */}
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>
773 </div>
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">
778 <input
779 type="checkbox"
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"
784 />
785 <span className="font-medium">Email To Supplier</span>
786 </label>
787 {selectedPO.f1203 && (
788 <button
789 onClick={() => setShowEmailOptions(!showEmailOptions)}
790 className="text-sm text-muted hover:text-brand cursor-pointer"
791 >
792 {showEmailOptions ? '▲' : '▼'} Options
793 </button>
794 )}
795 </div>
796
797 {selectedPO.f1203 && (
798 <div className="text-sm text-muted mb-3">
799 Default: {selectedPO.f1203}
800 </div>
801 )}
802
803 {showEmailOptions && (
804 <div className="space-y-3 mb-3">
805 <div>
806 <label className="block text-sm font-medium text-text mb-1">
807 Other Email (override)
808 </label>
809 <input
810 type="email"
811 value={otherEmail}
812 onChange={(e) => {
813 setOtherEmail(e.target.value);
814 if (e.target.value.length > 3) {
815 setSendViaEmail(true);
816 }
817 }}
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"
820 />
821 </div>
822
823 <div>
824 <label className="block text-sm font-medium text-text mb-1">
825 Additional Notes
826 </label>
827 <textarea
828 value={additionalNotes}
829 onChange={(e) => setAdditionalNotes(e.target.value)}
830 rows={3}
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"
833 />
834 <p className="text-xs text-muted mt-1 italic">
835 Additional notes require the template to support showing them
836 </p>
837 </div>
838 </div>
839 )}
840
841 <button
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"
845 >
846 » » Send Now « «
847 </button>
848 </div>
849
850 {/* Details */}
851 <div className="grid grid-cols-2 gap-4">
852 <div>
853 <p className="text-sm text-muted mb-1">Ships To</p>
854 <p className="font-medium">{selectedPO.f1105}</p>
855 </div>
856 <div>
857 <p className="text-sm text-muted mb-1">Created</p>
858 <p className="font-medium">{selectedPO.f106}</p>
859 </div>
860 <div>
861 <p className="text-sm text-muted mb-1">State</p>
862 <p className="font-medium">{selectedPO.f1114}</p>
863 </div>
864 <div>
865 <p className="text-sm text-muted mb-1">Email Status</p>
866 <p className="font-medium">
867 {selectedPO.f138 ? (
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>
873 ) : (
874 <span className="text-gray-500">Not Sent</span>
875 )}
876 </p>
877 </div>
878 <div>
879 <p className="text-sm text-muted mb-1">Supplier Ref</p>
880 <p className="font-medium">{selectedPO.f121 || "—"}</p>
881 </div>
882 {selectedPO.f137 && (
883 <div>
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>
886 </div>
887 )}
888 </div>
889
890 {/* Counts */}
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>
895 </div>
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>
899 </div>
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>
903 </div>
904 </div>
905
906 {/* Line Items Table */}
907 <div>
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">
912 <tr>
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>
920 </tr>
921 </thead>
922 <tbody>
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>
933 </tr>
934 ))
935 ) : (
936 <tr>
937 <td colSpan={7} className="px-3 py-4 text-center text-muted">No line items</td>
938 </tr>
939 )}
940 </tbody>
941 </table>
942 </div>
943 </div>
944
945 {/* Comments */}
946 <div className="space-y-4">
947 <div>
948 <label className="block text-sm font-medium text-text mb-2">Supplier Comments</label>
949 <textarea
950 rows={3}
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}
953 />
954 </div>
955 <div>
956 <label className="block text-sm font-medium text-text mb-2">Internal Comments</label>
957 <textarea
958 rows={3}
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}
961 />
962 </div>
963 </div>
964 </div>
965 </div>
966 </div>
967 )}
968 </div>
969 </div>
970 );
971}