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 React, { useState, useEffect } from 'react';
4import Link from 'next/link';
5
6interface EmailMessage {
7 physkey: string;
8 fromemail: string;
9 recvdtu: string;
10 processingstatus: number;
11 processingstatus_decoded: string;
12 processingstatus_decoded_bkcolor?: string;
13 subject: string;
14 messagelength: number;
15 _RowKey?: number;
16}
17
18interface EmailHeader {
19 name: string;
20 value: string;
21 seq: number;
22}
23
24interface EmailPart {
25 seq: number;
26 contenttype: string;
27 contenttransferencoding: string;
28 name: string;
29 partdata: string;
30 fdlreaderextract?: string;
31 fdlreadercontents?: string;
32 fdlreaderstatus?: number;
33}
34
35export default function InboundMessagesPage() {
36 const [messages, setMessages] = useState<EmailMessage[]>([]);
37 const [searchTerm, setSearchTerm] = useState('');
38 const [showNewArrival, setShowNewArrival] = useState(true);
39 const [showReady, setShowReady] = useState(true);
40 const [showDone, setShowDone] = useState(false);
41 const [showDeleting, setShowDeleting] = useState(false);
42 const [loading, setLoading] = useState(true);
43
44 // Modal states
45 const [viewModal, setViewModal] = useState(false);
46 const [headersModal, setHeadersModal] = useState(false);
47 const [selectedMessage, setSelectedMessage] = useState<EmailMessage | null>(null);
48 const [messageContent, setMessageContent] = useState('');
49 const [messageHeaders, setMessageHeaders] = useState<EmailHeader[]>([]);
50 const [messageParts, setMessageParts] = useState<EmailPart[]>([]);
51 const [viewedMalware, setViewedMalware] = useState(false);
52
53 useEffect(() => {
54 loadMessages();
55 }, []);
56
57 const loadMessages = async () => {
58 try {
59 const response = await fetch('/api/v1/messaging/inbound');
60
61 if (!response.ok || !response.headers.get('content-type')?.includes('application/json')) {
62 // Demo mode
63 setMessages(getDemoMessages());
64 setLoading(false);
65 return;
66 }
67
68 const data = await response.json();
69 setMessages(data.messages || []);
70 setLoading(false);
71 } catch (error) {
72 console.error('Error loading inbound messages:', error);
73 setMessages(getDemoMessages());
74 setLoading(false);
75 }
76 };
77
78 const getDemoMessages = (): EmailMessage[] => {
79 return [
80 {
81 physkey: 'msg_001',
82 fromemail: 'customer@example.com',
83 recvdtu: '2025-12-29 14:30:00',
84 processingstatus: 1,
85 processingstatus_decoded: 'Ready',
86 subject: 'Order inquiry - Product availability',
87 messagelength: 2048,
88 },
89 {
90 physkey: 'msg_002',
91 fromemail: 'supplier@warehouse.com',
92 recvdtu: '2025-12-29 13:15:00',
93 processingstatus: 3,
94 processingstatus_decoded: 'Completed',
95 subject: 'Invoice #INV-2025-001 attached',
96 messagelength: 15360,
97 },
98 {
99 physkey: 'msg_003',
100 fromemail: 'notifications@shipping.com',
101 recvdtu: '2025-12-29 12:45:00',
102 processingstatus: 0,
103 processingstatus_decoded: 'New Arrival',
104 subject: 'Delivery confirmation for order #12345',
105 messagelength: 1024,
106 },
107 ];
108 };
109
110 const getStatusBadgeColor = (status: number) => {
111 switch (status) {
112 case 0: return 'bg-blue-100 text-blue-800';
113 case 1: return 'bg-green-100 text-green-800';
114 case 2: return 'bg-red-100 text-red-800';
115 case 4: return 'bg-gray-100 text-gray-800';
116 case 256:
117 case 257:
118 return 'bg-orange-500 text-white';
119 default: return 'bg-gray-100 text-gray-800';
120 }
121 };
122
123 const decodeStatus = (status: number): { text: string; bgColor?: string } => {
124 switch (status) {
125 case 0: return { text: 'New Arrival' };
126 case 1: return { text: 'Ready' };
127 case 2: return { text: 'Deleting' };
128 case 4: return { text: 'Done' };
129 case 256: return { text: '***MALWARE DETECTED***', bgColor: 'orange' };
130 case 257: return { text: '***ATTACHMENT MALWARE DETECTED***', bgColor: 'orange' };
131 default: return { text: `Unknown (${status})` };
132 }
133 };
134
135 const filteredMessages = messages.filter(msg => {
136 // Status filter
137 const statusMap: { [key: number]: boolean } = {
138 0: showNewArrival,
139 1: showReady,
140 2: showDeleting,
141 4: showDone,
142 };
143
144 if (statusMap[msg.processingstatus] === false) return false;
145
146 // Search filter
147 if (searchTerm) {
148 const search = searchTerm.toLowerCase();
149 return (
150 msg.fromemail.toLowerCase().includes(search) ||
151 msg.subject.toLowerCase().includes(search)
152 );
153 }
154
155 return true;
156 });
157
158 const handleViewMessage = async (msg: EmailMessage) => {
159 // Check for malware
160 if ((msg.processingstatus >= 256 && msg.processingstatus < 512) && !viewedMalware) {
161 if (!confirm("This message has failed antivirus checks. It could be dangerous to your computer.\n\nDo you really wish to view it?")) {
162 return;
163 }
164 if (!confirm("Are you really sure you want to continue?\n\nIt has been recorded that you have viewed this against advice.\n\nSupport due to damage may be chargeable")) {
165 return;
166 }
167 setViewedMalware(true);
168 }
169
170 setSelectedMessage(msg);
171 setMessageContent('Loading message content...');
172 setMessageParts([]);
173 setViewModal(true);
174
175 // Fetch message content and parts
176 try {
177 const response = await fetch(`/api/v1/messaging/inbound/${msg.physkey}/content`);
178
179 if (!response.ok || !response.headers.get('content-type')?.includes('application/json')) {
180 // Demo content
181 setMessageContent(`Demo message content for: ${msg.subject}\n\nThis is a sample email body. In production, the actual email content would be displayed here.\n\nFrom: ${msg.fromemail}\nReceived: ${msg.recvdtu}`);
182 setMessageParts([
183 {
184 seq: 1,
185 contenttype: 'text/html',
186 contenttransferencoding: 'quoted-printable',
187 name: 'message.html',
188 partdata: 'Demo HTML content',
189 },
190 {
191 seq: 2,
192 contenttype: 'application/pdf',
193 contenttransferencoding: 'base64',
194 name: 'invoice.pdf',
195 partdata: 'base64encodeddata...',
196 fdlreaderextract: '{"invoice": "data"}',
197 },
198 ]);
199 return;
200 }
201
202 const data = await response.json();
203 setMessageContent(data.content || 'No content available');
204 setMessageParts(data.parts || []);
205 } catch (error) {
206 console.error('Error loading message content:', error);
207 setMessageContent('Error loading message content');
208 }
209 };
210
211 const handleViewHeaders = async (msg: EmailMessage) => {
212 setSelectedMessage(msg);
213 setMessageHeaders([]);
214 setHeadersModal(true);
215
216 try {
217 const response = await fetch(`/api/v1/messaging/inbound/${msg.physkey}/headers`);
218
219 if (!response.ok || !response.headers.get('content-type')?.includes('application/json')) {
220 // Demo headers
221 setMessageHeaders([
222 { name: 'From', value: msg.fromemail, seq: 1 },
223 { name: 'To', value: 'store@yourcompany.com', seq: 2 },
224 { name: 'Subject', value: msg.subject, seq: 3 },
225 { name: 'Date', value: msg.recvdtu, seq: 4 },
226 { name: 'Content-Type', value: 'multipart/mixed; boundary="boundary123"', seq: 5 },
227 { name: 'MIME-Version', value: '1.0', seq: 6 },
228 ]);
229 return;
230 }
231
232 const data = await response.json();
233 setMessageHeaders(data.headers || []);
234 } catch (error) {
235 console.error('Error loading headers:', error);
236 }
237 };
238
239 const handleReprocess = async (msg: EmailMessage, event: React.MouseEvent) => {
240 const ctrlKey = event.ctrlKey;
241
242 if (ctrlKey) {
243 if (!confirm('Reprocess ALL messages? This will reprocess every message in the list.')) {
244 return;
245 }
246 }
247
248 try {
249 const response = await fetch('/api/v1/messaging/inbound/reprocess', {
250 method: 'POST',
251 headers: { 'Content-Type': 'application/json' },
252 body: JSON.stringify({
253 physkey: ctrlKey ? 'all' : msg.physkey,
254 }),
255 });
256
257 if (!response.ok) {
258 alert('Reprocess queued (demo mode)');
259 return;
260 }
261
262 alert('Message reprocessing queued');
263 loadMessages();
264 } catch (error) {
265 console.error('Error reprocessing:', error);
266 alert('Reprocess queued (demo mode)');
267 }
268 };
269
270 const handleDelete = async (msg: EmailMessage) => {
271 if (!confirm(`Delete message from ${msg.fromemail}?\n\n${msg.subject}`)) {
272 return;
273 }
274
275 try {
276 const response = await fetch(`/api/v1/messaging/inbound/${msg.physkey}`, {
277 method: 'DELETE',
278 });
279
280 if (!response.ok) {
281 alert('Delete queued (demo mode)');
282 return;
283 }
284
285 alert('Message deletion queued');
286 loadMessages();
287 } catch (error) {
288 console.error('Error deleting:', error);
289 alert('Delete queued (demo mode)');
290 }
291 };
292
293 const formatSize = (bytes: number): string => {
294 return ((bytes + 512) / 1024).toFixed(2) + 'kb';
295 };
296
297 return (
298 <div className="container mx-auto px-4 py-8">
299 <div className="mb-6">
300 <div className="flex items-center gap-4 mb-4">
301 <Link href="/marketing/messaging" className="text-blue-600 hover:text-blue-800">
302 ← Back to Messaging
303 </Link>
304 <h1 className="text-3xl font-bold text-gray-900">Inbound Messages</h1>
305 </div>
306
307 <p className="text-gray-600 mb-4">
308 This page lists emails that have arrived and their automatic processing status.
309 Inbound email processing must be specifically configured and is typically only used
310 where Fieldpine has its own email account.
311 </p>
312 </div>
313
314 {/* Search and Filters */}
315 <div className="bg-white rounded-lg shadow-md p-6 mb-6">
316 <div className="flex flex-wrap items-center gap-4">
317 <div className="flex-1 min-w-[200px]">
318 <label className="block text-sm font-medium text-gray-700 mb-2">
319 Search (from/subject)
320 </label>
321 <input
322 type="text"
323 value={searchTerm}
324 onChange={(e) => setSearchTerm(e.target.value)}
325 placeholder="Filter by sender or subject..."
326 className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
327 />
328 </div>
329
330 <div className="flex flex-wrap items-center gap-4">
331 <span className="text-sm font-medium text-gray-700">Show:</span>
332
333 <label className="flex items-center gap-2 cursor-pointer">
334 <input
335 type="checkbox"
336 checked={showNewArrival}
337 onChange={(e) => setShowNewArrival(e.target.checked)}
338 className="w-4 h-4 text-blue-600 rounded focus:ring-blue-500"
339 />
340 <span className="text-sm text-gray-700">New Arrival</span>
341 </label>
342
343 <label className="flex items-center gap-2 cursor-pointer">
344 <input
345 type="checkbox"
346 checked={showReady}
347 onChange={(e) => setShowReady(e.target.checked)}
348 className="w-4 h-4 text-blue-600 rounded focus:ring-blue-500"
349 />
350 <span className="text-sm text-gray-700">Ready</span>
351 </label>
352
353 <label className="flex items-center gap-2 cursor-pointer">
354 <input
355 type="checkbox"
356 checked={showDone}
357 onChange={(e) => setShowDone(e.target.checked)}
358 className="w-4 h-4 text-blue-600 rounded focus:ring-blue-500"
359 />
360 <span className="text-sm text-gray-700">Done</span>
361 </label>
362
363 <label className="flex items-center gap-2 cursor-pointer">
364 <input
365 type="checkbox"
366 checked={showDeleting}
367 onChange={(e) => setShowDeleting(e.target.checked)}
368 className="w-4 h-4 text-blue-600 rounded focus:ring-blue-500"
369 />
370 <span className="text-sm text-gray-700">Deleting</span>
371 </label>
372 </div>
373 </div>
374 </div>
375
376 {/* Messages Table */}
377 <div className="bg-white rounded-lg shadow-md overflow-hidden">
378 <div className="overflow-x-auto">
379 <table className="min-w-full divide-y divide-gray-200">
380 <thead className="bg-gray-50">
381 <tr>
382 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
383 From
384 </th>
385 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
386 Received (UTC)
387 </th>
388 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
389 Status
390 </th>
391 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
392 Options
393 </th>
394 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
395 Subject
396 </th>
397 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
398 Size
399 </th>
400 </tr>
401 </thead>
402 <tbody className="bg-white divide-y divide-gray-200">
403 {loading ? (
404 <tr>
405 <td colSpan={6} className="px-6 py-8 text-center text-gray-500">
406 Loading messages...
407 </td>
408 </tr>
409 ) : filteredMessages.length === 0 ? (
410 <tr>
411 <td colSpan={6} className="px-6 py-8 text-center text-gray-500">
412 No messages found matching the current filters.
413 </td>
414 </tr>
415 ) : (
416 filteredMessages.map((msg, index) => (
417 <tr key={msg.physkey} className="hover:bg-gray-50">
418 <td className="px-6 py-4 whitespace-nowrap">
419 <div className="text-sm text-gray-900 max-w-xs truncate" title={msg.fromemail}>
420 {msg.fromemail}
421 </div>
422 </td>
423 <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
424 {msg.recvdtu}
425 </td>
426 <td className="px-6 py-4 whitespace-nowrap">
427 <span className={`px-2 py-1 inline-flex text-xs leading-5 font-semibold rounded-full ${getStatusBadgeColor(msg.processingstatus)}`}>
428 {msg.processingstatus_decoded}
429 </span>
430 </td>
431 <td className="px-6 py-4 whitespace-nowrap text-sm space-x-2">
432 <button
433 onClick={() => handleViewMessage(msg)}
434 className="text-blue-600 hover:text-blue-900 font-medium"
435 >
436 View
437 </button>
438 <button
439 onClick={() => handleViewHeaders(msg)}
440 className="text-blue-600 hover:text-blue-900 font-medium"
441 >
442 Headers
443 </button>
444 <button
445 onClick={(e) => handleReprocess(msg, e)}
446 className="text-green-600 hover:text-green-900 font-medium"
447 title="Hold Ctrl to reprocess all"
448 >
449 Reprocess
450 </button>
451 {msg.processingstatus !== 2 && (
452 <button
453 onClick={() => handleDelete(msg)}
454 className="text-red-600 hover:text-red-900 font-medium"
455 >
456 Delete
457 </button>
458 )}
459 </td>
460 <td className="px-6 py-4">
461 <div className="text-sm text-gray-900 max-w-md truncate" title={msg.subject}>
462 {msg.subject}
463 </div>
464 </td>
465 <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
466 {formatSize(msg.messagelength)}
467 </td>
468 </tr>
469 ))
470 )}
471 </tbody>
472 </table>
473 </div>
474 </div>
475
476 {/* View Message Modal */}
477 {viewModal && selectedMessage && (
478 <div
479 className="modal-overlay"
480 onClick={() => setViewModal(false)}
481 >
482 <div
483 className="bg-white rounded-lg shadow-xl max-w-6xl w-full max-h-[90vh] overflow-hidden"
484 onClick={(e) => e.stopPropagation()}
485 >
486 <div className="border-b border-gray-200 px-6 py-4 flex items-center justify-between">
487 <h2 className="text-xl font-semibold text-gray-900">
488 Message Contents
489 <span className="text-sm text-gray-500 ml-3">({selectedMessage.physkey})</span>
490 </h2>
491 <button
492 onClick={() => setViewModal(false)}
493 className="text-gray-400 hover:text-gray-600 text-2xl font-bold"
494 >
495 ×
496 </button>
497 </div>
498
499 <div className="p-6 overflow-y-auto max-h-[calc(90vh-120px)]">
500 {/* Message Parts */}
501 {messageParts.length > 0 && (
502 <div className="mb-6">
503 <h3 className="text-lg font-semibold mb-3">Attachments & Parts</h3>
504 <table className="min-w-full border border-gray-300">
505 <thead className="bg-gray-50">
506 <tr>
507 <th className="px-4 py-2 border-b text-left text-sm font-medium text-gray-700">Content Type</th>
508 <th className="px-4 py-2 border-b text-left text-sm font-medium text-gray-700">Encoding</th>
509 <th className="px-4 py-2 border-b text-left text-sm font-medium text-gray-700">Name</th>
510 <th className="px-4 py-2 border-b text-left text-sm font-medium text-gray-700">Options</th>
511 </tr>
512 </thead>
513 <tbody>
514 {messageParts.map((part, idx) => (
515 <tr key={idx} className="hover:bg-gray-50">
516 <td className="px-4 py-2 border-b text-sm">{part.contenttype}</td>
517 <td className="px-4 py-2 border-b text-sm">{part.contenttransferencoding}</td>
518 <td className="px-4 py-2 border-b text-sm">{part.name || '-'}</td>
519 <td className="px-4 py-2 border-b text-sm">
520 {part.fdlreaderextract && (
521 <button
522 onClick={() => alert(`Extract data:\n${part.fdlreaderextract}`)}
523 className="text-blue-600 hover:text-blue-800 text-sm mr-2"
524 >
525 View Extract
526 </button>
527 )}
528 {part.fdlreadercontents && (
529 <button
530 onClick={() => alert(`Contents:\n${part.fdlreadercontents}`)}
531 className="text-blue-600 hover:text-blue-800 text-sm"
532 >
533 View Contents
534 </button>
535 )}
536 </td>
537 </tr>
538 ))}
539 </tbody>
540 </table>
541 </div>
542 )}
543
544 {/* Message Content */}
545 <div className="mb-4">
546 <h3 className="text-lg font-semibold mb-3">Message Content</h3>
547 <pre className="bg-gray-50 p-4 rounded border border-gray-300 overflow-x-auto text-sm whitespace-pre-wrap">
548 {messageContent}
549 </pre>
550 </div>
551 </div>
552
553 <div className="border-t border-gray-200 px-6 py-4 flex justify-end">
554 <button
555 onClick={() => setViewModal(false)}
556 className="px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-700"
557 >
558 Close
559 </button>
560 </div>
561 </div>
562 </div>
563 )}
564
565 {/* View Headers Modal */}
566 {headersModal && selectedMessage && (
567 <div
568 className="modal-overlay"
569 onClick={() => setHeadersModal(false)}
570 >
571 <div
572 className="bg-white rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-hidden"
573 onClick={(e) => e.stopPropagation()}
574 >
575 <div className="border-b border-gray-200 px-6 py-4 flex items-center justify-between">
576 <h2 className="text-xl font-semibold text-gray-900">
577 Message Headers
578 <span className="text-sm text-gray-500 ml-3">({selectedMessage.physkey})</span>
579 </h2>
580 <button
581 onClick={() => setHeadersModal(false)}
582 className="text-gray-400 hover:text-gray-600 text-2xl font-bold"
583 >
584 ×
585 </button>
586 </div>
587
588 <div className="p-6 overflow-y-auto max-h-[calc(90vh-120px)]">
589 <table className="min-w-full border border-gray-300">
590 <thead className="bg-gray-50">
591 <tr>
592 <th className="px-4 py-2 border-b text-left text-sm font-medium text-gray-700">Name</th>
593 <th className="px-4 py-2 border-b text-left text-sm font-medium text-gray-700">Value</th>
594 </tr>
595 </thead>
596 <tbody>
597 {messageHeaders.map((header, idx) => (
598 <tr key={idx} className="hover:bg-gray-50">
599 <td className="px-4 py-2 border-b text-sm font-medium">{header.name}</td>
600 <td className="px-4 py-2 border-b text-sm break-all">{header.value}</td>
601 </tr>
602 ))}
603 </tbody>
604 </table>
605 </div>
606
607 <div className="border-t border-gray-200 px-6 py-4 flex justify-end">
608 <button
609 onClick={() => setHeadersModal(false)}
610 className="px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-700"
611 >
612 Close
613 </button>
614 </div>
615 </div>
616 </div>
617 )}
618 </div>
619 );
620}