3import { useState, useEffect, useRef } from 'react';
5interface DocumentProcessing {
10 DecodeStage_Word: string;
11 DecodedFileType: string;
15 InvoiceNumber: string;
19 f103?: string; // Comments
22type TabType = 'ready' | 'processing' | 'about';
24export default function UploadDocumentQueue() {
25 const [activeTab, setActiveTab] = useState<TabType>('ready');
26 const [readyDocs, setReadyDocs] = useState<DocumentProcessing[]>([]);
27 const [processingDocs, setProcessingDocs] = useState<DocumentProcessing[]>([]);
28 const [loading, setLoading] = useState(false);
29 const [uploading, setUploading] = useState(false);
30 const [isDragOver, setIsDragOver] = useState(false);
31 const [selectedDoc, setSelectedDoc] = useState<DocumentProcessing | null>(null);
32 const [showOptionsModal, setShowOptionsModal] = useState(false);
33 const [showUploadModal, setShowUploadModal] = useState(false);
34 const [enablePreview, setEnablePreview] = useState(true);
35 const [startDate, setStartDate] = useState('2020-01-01');
36 const [endDate, setEndDate] = useState('');
37 const [maxRows, setMaxRows] = useState('20');
38 const fileInputRef = useRef<HTMLInputElement>(null);
41 // Set end date to tomorrow
42 const tomorrow = new Date();
43 tomorrow.setDate(tomorrow.getDate() + 1);
44 setEndDate(tomorrow.toISOString().split('T')[0]);
46 // Load documents on mount
50 const loadDocuments = async () => {
54 let url = '/GNAP/J/BUCK?3=retailmax.elink.documentprocessing.list';
55 if (startDate) url += `&9=f101,ge,${startDate}`;
56 if (endDate) url += `&9=f101,lt,${endDate}`;
57 url += `&8=${maxRows}`;
59 // In production: fetch(url)
60 // For now, use sample data
61 console.log('Load documents from:', url);
63 // Simulate API call with sample data
64 const sampleReady: DocumentProcessing[] = [
66 Physkey: 'KEP3JWQYYBG4MZTDJS3AYSAAAAIQAAAB',
67 LoadDt: '2025-03-13T17:50:00',
68 SrcFileName: 'INVOICE_A3324989.PDF',
70 DecodeStage_Word: 'Ready',
71 DecodedFileType: 'PDF',
73 DocTypeName: 'Invoice',
75 InvoiceNumber: 'A3324989',
76 Issuer: 'Ausjet Holdings Pty',
82 Physkey: 'KEP3JWAYYBG4MZTDDIWQYBIAAAJQAAAB',
83 LoadDt: '2025-03-12T14:40:00',
84 SrcFileName: 'Print Invoice A3324949.pdf',
86 DecodeStage_Word: 'Ready',
87 DecodedFileType: 'PDF',
89 DocTypeName: 'Invoice',
91 InvoiceNumber: 'A3324949',
92 Issuer: 'Ausjet Holdings Pty',
98 Physkey: 'KEP3JWAYYBG4MZTDDIUQYBIAAAIQAAAB',
99 LoadDt: '2025-03-12T14:40:00',
100 SrcFileName: 'Print Invoice A3324895.pdf',
102 DecodeStage_Word: 'Ready',
103 DecodedFileType: 'PDF',
105 DocTypeName: 'Invoice',
107 InvoiceNumber: 'A3324895',
108 Issuer: 'Ausjet Holdings Pty',
115 const sampleProcessing: DocumentProcessing[] = [
117 Physkey: 'KEP3PROC1',
118 LoadDt: '2025-02-28T19:02:00',
119 SrcFileName: 'Print Invoice A3324351.pdf',
121 DecodeStage_Word: 'Analysing',
122 DecodedFileType: 'PDF'
125 Physkey: 'KEP3PROC2',
126 LoadDt: '2025-02-28T19:01:00',
127 SrcFileName: 'Print Invoice A3324685.pdf',
129 DecodeStage_Word: 'Analysing',
130 DecodedFileType: 'PDF'
133 Physkey: 'KEP3PROC3',
134 LoadDt: '2025-02-28T18:57:00',
135 SrcFileName: 'Print Invoice A3324684.pdf',
137 DecodeStage_Word: 'Analysing',
138 DecodedFileType: 'PDF'
142 setReadyDocs(sampleReady);
143 setProcessingDocs(sampleProcessing);
145 console.error('Error loading documents:', error);
146 alert('Error loading documents. Please try again.');
152 const handleUploadFile = async (file: File) => {
154 setShowUploadModal(false);
157 const formData = new FormData();
158 formData.append('f8', 'retailmax.elink.documentprocessing.load');
159 formData.append('f11_B', 'E');
160 formData.append('f135_E', '1'); // Queue
161 formData.append('f110/n301', file);
164 // const response = await fetch('/gnap/J/dati', {
168 // 'X-Api-Key': sessionStorage.getItem('LginApiKey') || ''
172 console.log('Upload file:', file.name);
173 alert(`File "${file.name}" uploaded successfully!\n\nThe document will be processed and appear in the queue shortly.`);
175 // Reload documents after short delay
180 console.error('Error uploading file:', error);
181 alert('Error uploading file. Please try again.');
187 const handleDragOver = (e: React.DragEvent) => {
193 const handleDragLeave = (e: React.DragEvent) => {
196 setIsDragOver(false);
199 const handleDrop = (e: React.DragEvent) => {
202 setIsDragOver(false);
204 const files = e.dataTransfer.files;
205 if (files && files.length > 0) {
206 if (files.length > 1) {
207 alert('Please drop only one file at a time');
210 handleUploadFile(files[0]);
214 const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
215 const files = e.target.files;
216 if (files && files.length > 0) {
217 handleUploadFile(files[0]);
221 const openUploadModal = () => {
222 setShowUploadModal(true);
225 const closeUploadModal = () => {
226 setShowUploadModal(false);
229 const openOptionsModal = (doc: DocumentProcessing) => {
231 setShowOptionsModal(true);
234 const closeOptionsModal = () => {
235 setShowOptionsModal(false);
236 setSelectedDoc(null);
239 const handleRejectDocument = async () => {
240 if (!selectedDoc) return;
242 if (confirm('Really reject this document?\n\nIt will not be processed any further.')) {
243 // Fieldpine command structure
245 CommandText: `:stock document ${selectedDoc.Physkey} mark rejected`
248 // In production: await fetch('/OpenApi/Stock2/Primitive', { method: 'POST', body: JSON.stringify(command) });
249 console.log('Reject document:', command);
250 alert('Document rejected');
256 const handleRescanDocument = async () => {
257 if (!selectedDoc) return;
259 if (confirm('Rescan this document?\n\nThe document will be reset as if you had just now uploaded it.\nYou may be charged again for this rescanning.')) {
261 CommandText: `:stock document ${selectedDoc.Physkey} mark rerun`
264 // In production: await fetch('/OpenApi/Stock2/Primitive', { method: 'POST', body: JSON.stringify(command) });
265 console.log('Rescan document:', command);
266 alert('Document queued for rescanning');
272 const handleReceiveOrder = (physkey: string) => {
273 // Navigate to receive page
274 window.location.href = `/purchase-orders/receivedoc?wq=${physkey}`;
277 const formatDateTime = (dateStr: string) => {
278 if (!dateStr) return '';
279 const date = new Date(dateStr);
280 return date.toLocaleString('en-GB', {
290 <div className="min-h-screen bg-surface p-6">
291 <div className="max-w-7xl mx-auto">
293 <div className="mb-6">
294 <div className="flex items-center gap-4 mb-4">
296 onClick={() => window.history.back()}
297 className="text-brand hover:underline cursor-pointer"
299 ← Back to Purchase Orders
302 <h1 className="text-3xl font-bold text-brand mb-2">Document Work Queue</h1>
303 <p className="text-muted">
304 Manage inbound documents such as PDF files, invoice scans and other paperwork.
305 Fieldpine will attempt to decode and process this paperwork automatically.
307 <p className="text-sm text-muted mt-2">
308 NOTE: Currently only a limited set of suppliers and documents can be processed.
309 <a href="https://fieldpine.com/docs/howdoi/receivescan.htm" target="_blank" className="text-brand ml-2 hover:underline">User Guide</a>
313 {/* Upload Drop Zone */}
315 className={`card p-6 mb-6 border-2 border-dashed transition-all cursor-pointer ${
316 isDragOver ? 'border-brand bg-green-50' : 'border-border'
318 onDragOver={handleDragOver}
319 onDragLeave={handleDragLeave}
321 onClick={openUploadModal}
323 <div className="text-center">
324 <div className="text-4xl mb-2">📄</div>
325 <p className="font-semibold mb-1">Drop New Documents Here</p>
326 <p className="text-sm text-muted">
327 Documents will be sent to Fieldpine.com for analysis
330 className="mt-3 px-4 py-2 text-white rounded hover:opacity-80 transition-all cursor-pointer bg-brand"
331 onClick={(e) => { e.stopPropagation(); openUploadModal(); }}
339 <div className="card p-4 mb-6">
340 <div className="flex flex-wrap gap-4 items-end">
342 <label className="block text-sm font-medium mb-1">From Date (incl)</label>
346 onChange={(e) => setStartDate(e.target.value)}
347 className="border border-border rounded px-3 py-2"
351 <label className="block text-sm font-medium mb-1">To Date (excl)</label>
355 onChange={(e) => setEndDate(e.target.value)}
356 className="border border-border rounded px-3 py-2"
360 <label className="block text-sm font-medium mb-1">Max Rows</label>
363 onChange={(e) => setMaxRows(e.target.value)}
364 className="border border-border rounded px-3 py-2"
366 <option value="10">10</option>
367 <option value="20">20</option>
368 <option value="50">50</option>
369 <option value="100">100</option>
370 <option value="500">500</option>
374 onClick={loadDocuments}
376 className="px-4 py-2 text-white rounded hover:opacity-80 transition-all cursor-pointer bg-brand"
378 {loading ? 'Loading...' : 'Load'}
380 <label className="flex items-center gap-2 ml-auto">
383 checked={enablePreview}
384 onChange={(e) => setEnablePreview(e.target.checked)}
386 <span className="text-sm">Show Preview</span>
392 <div className="mb-6">
393 <div className="border-b border-border flex gap-1">
395 onClick={() => setActiveTab('ready')}
396 className={`px-4 py-2 font-medium transition-colors cursor-pointer ${
397 activeTab === 'ready'
398 ? 'border-b-2 border-brand text-brand'
399 : 'text-muted hover:text-text'
402 Ready For Use <span className="text-sm">({readyDocs.length})</span>
405 onClick={() => setActiveTab('processing')}
406 className={`px-4 py-2 font-medium transition-colors cursor-pointer ${
407 activeTab === 'processing'
408 ? 'border-b-2 border-brand text-brand'
409 : 'text-muted hover:text-text'
412 Still Processing <span className="text-sm">({processingDocs.length})</span>
415 onClick={() => setActiveTab('about')}
416 className={`px-4 py-2 font-medium transition-colors cursor-pointer ${
417 activeTab === 'about'
418 ? 'border-b-2 border-brand text-brand'
419 : 'text-muted hover:text-text'
428 {activeTab === 'ready' && (
429 <div className="card overflow-x-auto">
430 <table className="w-full text-sm">
432 <tr className="border-b border-border">
433 <th className="text-left py-3 px-4 font-semibold">Received Date</th>
434 <th className="text-left py-3 px-4 font-semibold">Content</th>
435 <th className="text-left py-3 px-4 font-semibold">Doc Date</th>
436 <th className="text-left py-3 px-4 font-semibold">Invoice#</th>
437 <th className="text-left py-3 px-4 font-semibold">Issuer</th>
438 <th className="text-left py-3 px-4 font-semibold">Value</th>
439 <th className="text-left py-3 px-4 font-semibold">Comments</th>
440 <th className="text-left py-3 px-4 font-semibold">Options</th>
441 <th className="text-left py-3 px-4 font-semibold">Original FileName</th>
445 {readyDocs.map((doc, idx) => (
446 <tr key={doc.Physkey} className={idx % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
447 <td className="py-3 px-4">{formatDateTime(doc.LoadDt)}</td>
448 <td className="py-3 px-4">{doc.DOCU?.[0]?.DocTypeName || ''}</td>
449 <td className="py-3 px-4">{doc.DOCU?.[0]?.Date || ''}</td>
450 <td className="py-3 px-4">{doc.DOCU?.[0]?.InvoiceNumber || ''}</td>
451 <td className="py-3 px-4">{doc.DOCU?.[0]?.Issuer || ''}</td>
452 <td className="py-3 px-4">${doc.DOCU?.[0]?.GrandTotal?.toFixed(2) || '0.00'}</td>
453 <td className="py-3 px-4">{doc.f103 || ''}</td>
454 <td className="py-3 px-4">
455 <div className="flex gap-2">
457 onClick={() => openOptionsModal(doc)}
458 className="px-3 py-1 text-sm border border-border rounded hover:bg-gray-100 cursor-pointer"
463 onClick={() => handleReceiveOrder(doc.Physkey)}
464 className="px-3 py-1 text-sm text-white rounded hover:opacity-80 cursor-pointer bg-brand"
470 <td className="py-3 px-4 text-sm text-muted">{doc.SrcFileName}</td>
473 {readyDocs.length === 0 && (
475 <td colSpan={9} className="py-8 text-center text-muted">
476 No documents ready for use
485 {activeTab === 'processing' && (
486 <div className="card overflow-x-auto">
487 <table className="w-full text-sm">
489 <tr className="border-b border-border">
490 <th className="text-left py-3 px-4 font-semibold">Load Date</th>
491 <th className="text-left py-3 px-4 font-semibold">Original FileName</th>
492 <th className="text-left py-3 px-4 font-semibold">DecodeStage</th>
493 <th className="text-left py-3 px-4 font-semibold">DecodedFileType</th>
494 <th className="text-left py-3 px-4 font-semibold">Options</th>
498 {processingDocs.map((doc, idx) => (
499 <tr key={doc.Physkey} className={idx % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
500 <td className="py-3 px-4">{formatDateTime(doc.LoadDt)}</td>
501 <td className="py-3 px-4">{doc.SrcFileName}</td>
502 <td className="py-3 px-4">{doc.DecodeStage_Word}</td>
503 <td className="py-3 px-4">{doc.DecodedFileType}</td>
504 <td className="py-3 px-4">
506 onClick={() => openOptionsModal(doc)}
507 className="px-3 py-1 text-sm border border-border rounded hover:bg-gray-100 cursor-pointer"
514 {processingDocs.length === 0 && (
516 <td colSpan={5} className="py-8 text-center text-muted">
517 No documents currently processing
526 {activeTab === 'about' && (
527 <div className="card p-6">
528 <h2 className="text-xl font-bold mb-4">About Document Processing</h2>
530 This page shows documents that have been loaded or are currently being processed through the system.
531 As various documents arrive in the form of Emails, XML files, photographs and scanned images they are
532 placed in the work queue for processing.
535 <h3 className="font-bold mt-6 mb-3">Processing Workflow</h3>
536 <div className="space-y-4 mb-6">
537 <div className="border border-brand rounded p-4" style={{ backgroundColor: '#ffffa0' }}>
538 <h4 className="font-semibold mb-2">1. Acquire Document</h4>
539 <p className="text-sm">Image Scanner • Digital Photograph • Excel • Email • Electronic Feeds</p>
541 <div className="border border-brand rounded p-4" style={{ backgroundColor: '#a0a0ff' }}>
542 <h4 className="font-semibold mb-2">2. Document Loaded</h4>
543 <p className="text-sm">OCR Analysis • Content Analysis</p>
545 <div className="border border-brand rounded p-4" style={{ backgroundColor: '#a0ffa0' }}>
546 <h4 className="font-semibold mb-2">3. Await Human Confirmation</h4>
547 <p className="text-sm">Apply to System</p>
551 <h3 className="font-bold mt-6 mb-3">Tips and Tricks</h3>
553 The system works by 'reading' the document much like a human. It matches that to patterns and attempts
554 to determine the contents. While this works well when operational, it is somewhat fragile as the document
555 you receive is not formally defined and can subtly change without warning.
558 <h4 className="font-semibold mb-2">Recommended File Formats (in order of preference):</h4>
559 <ul className="list-disc ml-6 mb-4">
564 <li>PDF (not recommended for scans, as is typically simply a photo store saved into a PDF file)</li>
568 If you are taking photographs of documents, please try and use a high resolution camera. Photographs
569 taken with iPad cameras are often not processable due to resolution and aggressive compression. If
570 photographing documents, try to hold the document at right angles and photograph square on.
576 {showUploadModal && (
577 <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50" onClick={closeUploadModal}>
578 <div className="bg-white rounded-lg p-6 max-w-md w-full mx-4" onClick={(e) => e.stopPropagation()}>
579 <div className="flex justify-between items-center mb-4">
580 <h2 className="text-xl font-bold">Document Upload</h2>
581 <button onClick={closeUploadModal} className="text-2xl hover:text-brand cursor-pointer">×</button>
583 <p className="mb-4">Select a file containing the invoice</p>
587 onChange={handleFileSelect}
588 accept=".pdf,.png,.jpg,.jpeg,.tiff,.bmp"
589 className="mb-4 w-full"
591 <div className="flex justify-end gap-2">
593 onClick={closeUploadModal}
594 className="px-4 py-2 border border-border rounded hover:bg-gray-100 cursor-pointer"
599 onClick={() => fileInputRef.current?.click()}
600 className="px-4 py-2 text-white rounded hover:opacity-80 cursor-pointer bg-brand"
609 {/* Options Modal */}
610 {showOptionsModal && selectedDoc && (
611 <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50" onClick={closeOptionsModal}>
612 <div className="bg-white rounded-lg p-6 max-w-md w-full mx-4" onClick={(e) => e.stopPropagation()}>
613 <div className="flex justify-between items-center mb-4">
614 <h2 className="text-xl font-bold">Document Options</h2>
615 <button onClick={closeOptionsModal} className="text-2xl hover:text-brand cursor-pointer">×</button>
617 <div className="space-y-4">
618 {selectedDoc.DOCU?.[0]?.InvoiceNumber && (
620 <span className="font-semibold">Invoice: </span>
621 <span>{selectedDoc.DOCU[0].InvoiceNumber}</span>
624 <div className="border-t pt-4 space-y-3">
625 <div className="flex gap-3">
627 onClick={handleRejectDocument}
628 className="flex-1 px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 cursor-pointer"
632 <p className="flex-1 text-sm text-muted">
633 Reject this document. It will not be processed any further
636 <div className="flex gap-3">
638 onClick={handleRescanDocument}
639 className="flex-1 px-4 py-2 border border-border rounded hover:bg-gray-100 cursor-pointer"
643 <p className="flex-1 text-sm text-muted">
644 Rescan this document. The document will be reset as if you had just now uploaded it. You may be charged again.
653 {/* Uploading Overlay */}
655 <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
656 <div className="bg-white rounded-lg p-6">
657 <div className="flex items-center gap-3">
658 <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-brand"></div>
659 <p className="font-semibold">Processing... Please Wait</p>