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 { useState, useEffect, useRef } from 'react';
4
5interface DocumentProcessing {
6 Physkey: string;
7 LoadDt: string;
8 SrcFileName: string;
9 DecodeStage: number;
10 DecodeStage_Word: string;
11 DecodedFileType: string;
12 DOCU?: Array<{
13 DocTypeName: string;
14 Date: string;
15 InvoiceNumber: string;
16 Issuer: string;
17 GrandTotal: number;
18 }>;
19 f103?: string; // Comments
20}
21
22type TabType = 'ready' | 'processing' | 'about';
23
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);
39
40 useEffect(() => {
41 // Set end date to tomorrow
42 const tomorrow = new Date();
43 tomorrow.setDate(tomorrow.getDate() + 1);
44 setEndDate(tomorrow.toISOString().split('T')[0]);
45
46 // Load documents on mount
47 loadDocuments();
48 }, []);
49
50 const loadDocuments = async () => {
51 setLoading(true);
52 try {
53 // Build query params
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}`;
58
59 // In production: fetch(url)
60 // For now, use sample data
61 console.log('Load documents from:', url);
62
63 // Simulate API call with sample data
64 const sampleReady: DocumentProcessing[] = [
65 {
66 Physkey: 'KEP3JWQYYBG4MZTDJS3AYSAAAAIQAAAB',
67 LoadDt: '2025-03-13T17:50:00',
68 SrcFileName: 'INVOICE_A3324989.PDF',
69 DecodeStage: 19,
70 DecodeStage_Word: 'Ready',
71 DecodedFileType: 'PDF',
72 DOCU: [{
73 DocTypeName: 'Invoice',
74 Date: '2025-03-13',
75 InvoiceNumber: 'A3324989',
76 Issuer: 'Ausjet Holdings Pty',
77 GrandTotal: 499.63
78 }],
79 f103: ''
80 },
81 {
82 Physkey: 'KEP3JWAYYBG4MZTDDIWQYBIAAAJQAAAB',
83 LoadDt: '2025-03-12T14:40:00',
84 SrcFileName: 'Print Invoice A3324949.pdf',
85 DecodeStage: 19,
86 DecodeStage_Word: 'Ready',
87 DecodedFileType: 'PDF',
88 DOCU: [{
89 DocTypeName: 'Invoice',
90 Date: '',
91 InvoiceNumber: 'A3324949',
92 Issuer: 'Ausjet Holdings Pty',
93 GrandTotal: 976.21
94 }],
95 f103: ''
96 },
97 {
98 Physkey: 'KEP3JWAYYBG4MZTDDIUQYBIAAAIQAAAB',
99 LoadDt: '2025-03-12T14:40:00',
100 SrcFileName: 'Print Invoice A3324895.pdf',
101 DecodeStage: 19,
102 DecodeStage_Word: 'Ready',
103 DecodedFileType: 'PDF',
104 DOCU: [{
105 DocTypeName: 'Invoice',
106 Date: '2025-03-12',
107 InvoiceNumber: 'A3324895',
108 Issuer: 'Ausjet Holdings Pty',
109 GrandTotal: 863.67
110 }],
111 f103: ''
112 }
113 ];
114
115 const sampleProcessing: DocumentProcessing[] = [
116 {
117 Physkey: 'KEP3PROC1',
118 LoadDt: '2025-02-28T19:02:00',
119 SrcFileName: 'Print Invoice A3324351.pdf',
120 DecodeStage: 42,
121 DecodeStage_Word: 'Analysing',
122 DecodedFileType: 'PDF'
123 },
124 {
125 Physkey: 'KEP3PROC2',
126 LoadDt: '2025-02-28T19:01:00',
127 SrcFileName: 'Print Invoice A3324685.pdf',
128 DecodeStage: 42,
129 DecodeStage_Word: 'Analysing',
130 DecodedFileType: 'PDF'
131 },
132 {
133 Physkey: 'KEP3PROC3',
134 LoadDt: '2025-02-28T18:57:00',
135 SrcFileName: 'Print Invoice A3324684.pdf',
136 DecodeStage: 42,
137 DecodeStage_Word: 'Analysing',
138 DecodedFileType: 'PDF'
139 }
140 ];
141
142 setReadyDocs(sampleReady);
143 setProcessingDocs(sampleProcessing);
144 } catch (error) {
145 console.error('Error loading documents:', error);
146 alert('Error loading documents. Please try again.');
147 } finally {
148 setLoading(false);
149 }
150 };
151
152 const handleUploadFile = async (file: File) => {
153 setUploading(true);
154 setShowUploadModal(false);
155
156 try {
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);
162
163 // In production:
164 // const response = await fetch('/gnap/J/dati', {
165 // method: 'POST',
166 // body: formData,
167 // headers: {
168 // 'X-Api-Key': sessionStorage.getItem('LginApiKey') || ''
169 // }
170 // });
171
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.`);
174
175 // Reload documents after short delay
176 setTimeout(() => {
177 loadDocuments();
178 }, 2000);
179 } catch (error) {
180 console.error('Error uploading file:', error);
181 alert('Error uploading file. Please try again.');
182 } finally {
183 setUploading(false);
184 }
185 };
186
187 const handleDragOver = (e: React.DragEvent) => {
188 e.preventDefault();
189 e.stopPropagation();
190 setIsDragOver(true);
191 };
192
193 const handleDragLeave = (e: React.DragEvent) => {
194 e.preventDefault();
195 e.stopPropagation();
196 setIsDragOver(false);
197 };
198
199 const handleDrop = (e: React.DragEvent) => {
200 e.preventDefault();
201 e.stopPropagation();
202 setIsDragOver(false);
203
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');
208 return;
209 }
210 handleUploadFile(files[0]);
211 }
212 };
213
214 const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
215 const files = e.target.files;
216 if (files && files.length > 0) {
217 handleUploadFile(files[0]);
218 }
219 };
220
221 const openUploadModal = () => {
222 setShowUploadModal(true);
223 };
224
225 const closeUploadModal = () => {
226 setShowUploadModal(false);
227 };
228
229 const openOptionsModal = (doc: DocumentProcessing) => {
230 setSelectedDoc(doc);
231 setShowOptionsModal(true);
232 };
233
234 const closeOptionsModal = () => {
235 setShowOptionsModal(false);
236 setSelectedDoc(null);
237 };
238
239 const handleRejectDocument = async () => {
240 if (!selectedDoc) return;
241
242 if (confirm('Really reject this document?\n\nIt will not be processed any further.')) {
243 // Fieldpine command structure
244 const command = {
245 CommandText: `:stock document ${selectedDoc.Physkey} mark rejected`
246 };
247
248 // In production: await fetch('/OpenApi/Stock2/Primitive', { method: 'POST', body: JSON.stringify(command) });
249 console.log('Reject document:', command);
250 alert('Document rejected');
251 closeOptionsModal();
252 loadDocuments();
253 }
254 };
255
256 const handleRescanDocument = async () => {
257 if (!selectedDoc) return;
258
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.')) {
260 const command = {
261 CommandText: `:stock document ${selectedDoc.Physkey} mark rerun`
262 };
263
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');
267 closeOptionsModal();
268 loadDocuments();
269 }
270 };
271
272 const handleReceiveOrder = (physkey: string) => {
273 // Navigate to receive page
274 window.location.href = `/purchase-orders/receivedoc?wq=${physkey}`;
275 };
276
277 const formatDateTime = (dateStr: string) => {
278 if (!dateStr) return '';
279 const date = new Date(dateStr);
280 return date.toLocaleString('en-GB', {
281 day: '2-digit',
282 month: 'short',
283 year: 'numeric',
284 hour: '2-digit',
285 minute: '2-digit'
286 });
287 };
288
289 return (
290 <div className="min-h-screen bg-surface p-6">
291 <div className="max-w-7xl mx-auto">
292 {/* Header */}
293 <div className="mb-6">
294 <div className="flex items-center gap-4 mb-4">
295 <button
296 onClick={() => window.history.back()}
297 className="text-brand hover:underline cursor-pointer"
298 >
299 ← Back to Purchase Orders
300 </button>
301 </div>
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.
306 </p>
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>
310 </p>
311 </div>
312
313 {/* Upload Drop Zone */}
314 <div
315 className={`card p-6 mb-6 border-2 border-dashed transition-all cursor-pointer ${
316 isDragOver ? 'border-brand bg-green-50' : 'border-border'
317 }`}
318 onDragOver={handleDragOver}
319 onDragLeave={handleDragLeave}
320 onDrop={handleDrop}
321 onClick={openUploadModal}
322 >
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
328 </p>
329 <button
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(); }}
332 >
333 Find File
334 </button>
335 </div>
336 </div>
337
338 {/* Filters */}
339 <div className="card p-4 mb-6">
340 <div className="flex flex-wrap gap-4 items-end">
341 <div>
342 <label className="block text-sm font-medium mb-1">From Date (incl)</label>
343 <input
344 type="date"
345 value={startDate}
346 onChange={(e) => setStartDate(e.target.value)}
347 className="border border-border rounded px-3 py-2"
348 />
349 </div>
350 <div>
351 <label className="block text-sm font-medium mb-1">To Date (excl)</label>
352 <input
353 type="date"
354 value={endDate}
355 onChange={(e) => setEndDate(e.target.value)}
356 className="border border-border rounded px-3 py-2"
357 />
358 </div>
359 <div>
360 <label className="block text-sm font-medium mb-1">Max Rows</label>
361 <select
362 value={maxRows}
363 onChange={(e) => setMaxRows(e.target.value)}
364 className="border border-border rounded px-3 py-2"
365 >
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>
371 </select>
372 </div>
373 <button
374 onClick={loadDocuments}
375 disabled={loading}
376 className="px-4 py-2 text-white rounded hover:opacity-80 transition-all cursor-pointer bg-brand"
377 >
378 {loading ? 'Loading...' : 'Load'}
379 </button>
380 <label className="flex items-center gap-2 ml-auto">
381 <input
382 type="checkbox"
383 checked={enablePreview}
384 onChange={(e) => setEnablePreview(e.target.checked)}
385 />
386 <span className="text-sm">Show Preview</span>
387 </label>
388 </div>
389 </div>
390
391 {/* Tabs */}
392 <div className="mb-6">
393 <div className="border-b border-border flex gap-1">
394 <button
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'
400 }`}
401 >
402 Ready For Use <span className="text-sm">({readyDocs.length})</span>
403 </button>
404 <button
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'
410 }`}
411 >
412 Still Processing <span className="text-sm">({processingDocs.length})</span>
413 </button>
414 <button
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'
420 }`}
421 >
422 About
423 </button>
424 </div>
425 </div>
426
427 {/* Tab Content */}
428 {activeTab === 'ready' && (
429 <div className="card overflow-x-auto">
430 <table className="w-full text-sm">
431 <thead>
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>
442 </tr>
443 </thead>
444 <tbody>
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">
456 <button
457 onClick={() => openOptionsModal(doc)}
458 className="px-3 py-1 text-sm border border-border rounded hover:bg-gray-100 cursor-pointer"
459 >
460 Options
461 </button>
462 <button
463 onClick={() => handleReceiveOrder(doc.Physkey)}
464 className="px-3 py-1 text-sm text-white rounded hover:opacity-80 cursor-pointer bg-brand"
465 >
466 Receive Order
467 </button>
468 </div>
469 </td>
470 <td className="py-3 px-4 text-sm text-muted">{doc.SrcFileName}</td>
471 </tr>
472 ))}
473 {readyDocs.length === 0 && (
474 <tr>
475 <td colSpan={9} className="py-8 text-center text-muted">
476 No documents ready for use
477 </td>
478 </tr>
479 )}
480 </tbody>
481 </table>
482 </div>
483 )}
484
485 {activeTab === 'processing' && (
486 <div className="card overflow-x-auto">
487 <table className="w-full text-sm">
488 <thead>
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>
495 </tr>
496 </thead>
497 <tbody>
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">
505 <button
506 onClick={() => openOptionsModal(doc)}
507 className="px-3 py-1 text-sm border border-border rounded hover:bg-gray-100 cursor-pointer"
508 >
509 Options
510 </button>
511 </td>
512 </tr>
513 ))}
514 {processingDocs.length === 0 && (
515 <tr>
516 <td colSpan={5} className="py-8 text-center text-muted">
517 No documents currently processing
518 </td>
519 </tr>
520 )}
521 </tbody>
522 </table>
523 </div>
524 )}
525
526 {activeTab === 'about' && (
527 <div className="card p-6">
528 <h2 className="text-xl font-bold mb-4">About Document Processing</h2>
529 <p className="mb-4">
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.
533 </p>
534
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>
540 </div>
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>
544 </div>
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>
548 </div>
549 </div>
550
551 <h3 className="font-bold mt-6 mb-3">Tips and Tricks</h3>
552 <p className="mb-4">
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.
556 </p>
557
558 <h4 className="font-semibold mb-2">Recommended File Formats (in order of preference):</h4>
559 <ul className="list-disc ml-6 mb-4">
560 <li>TIFF</li>
561 <li>PNG</li>
562 <li>BMP</li>
563 <li>JPEG</li>
564 <li>PDF (not recommended for scans, as is typically simply a photo store saved into a PDF file)</li>
565 </ul>
566
567 <p className="mb-4">
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.
571 </p>
572 </div>
573 )}
574
575 {/* Upload Modal */}
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">&times;</button>
582 </div>
583 <p className="mb-4">Select a file containing the invoice</p>
584 <input
585 ref={fileInputRef}
586 type="file"
587 onChange={handleFileSelect}
588 accept=".pdf,.png,.jpg,.jpeg,.tiff,.bmp"
589 className="mb-4 w-full"
590 />
591 <div className="flex justify-end gap-2">
592 <button
593 onClick={closeUploadModal}
594 className="px-4 py-2 border border-border rounded hover:bg-gray-100 cursor-pointer"
595 >
596 Cancel
597 </button>
598 <button
599 onClick={() => fileInputRef.current?.click()}
600 className="px-4 py-2 text-white rounded hover:opacity-80 cursor-pointer bg-brand"
601 >
602 Select File
603 </button>
604 </div>
605 </div>
606 </div>
607 )}
608
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">&times;</button>
616 </div>
617 <div className="space-y-4">
618 {selectedDoc.DOCU?.[0]?.InvoiceNumber && (
619 <div>
620 <span className="font-semibold">Invoice: </span>
621 <span>{selectedDoc.DOCU[0].InvoiceNumber}</span>
622 </div>
623 )}
624 <div className="border-t pt-4 space-y-3">
625 <div className="flex gap-3">
626 <button
627 onClick={handleRejectDocument}
628 className="flex-1 px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 cursor-pointer"
629 >
630 Reject Document
631 </button>
632 <p className="flex-1 text-sm text-muted">
633 Reject this document. It will not be processed any further
634 </p>
635 </div>
636 <div className="flex gap-3">
637 <button
638 onClick={handleRescanDocument}
639 className="flex-1 px-4 py-2 border border-border rounded hover:bg-gray-100 cursor-pointer"
640 >
641 Rescan Document
642 </button>
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.
645 </p>
646 </div>
647 </div>
648 </div>
649 </div>
650 </div>
651 )}
652
653 {/* Uploading Overlay */}
654 {uploading && (
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>
660 </div>
661 </div>
662 </div>
663 )}
664 </div>
665 </div>
666 );
667}