From e45f4a777f7f8d189e75697bedd92dfac1a95c0c Mon Sep 17 00:00:00 2001 From: chark1es Date: Tue, 11 Mar 2025 02:33:31 -0700 Subject: [PATCH] added invoice viweing on event request management --- .../EventRequestDetails.tsx | 531 +++++++++++++++++- .../EventRequestManagementTable.tsx | 1 + 2 files changed, 531 insertions(+), 1 deletion(-) diff --git a/src/components/dashboard/Officer_EventRequestManagement/EventRequestDetails.tsx b/src/components/dashboard/Officer_EventRequestManagement/EventRequestDetails.tsx index 8bf7bb3..26c95b8 100644 --- a/src/components/dashboard/Officer_EventRequestManagement/EventRequestDetails.tsx +++ b/src/components/dashboard/Officer_EventRequestManagement/EventRequestDetails.tsx @@ -4,6 +4,7 @@ import toast from 'react-hot-toast'; import { DataSyncService } from '../../../scripts/database/DataSyncService'; import { Collections } from '../../../schemas/pocketbase/schema'; import type { EventRequest as SchemaEventRequest } from '../../../schemas/pocketbase/schema'; +import { FileManager } from '../../../scripts/pocketbase/FileManager'; // Extended EventRequest interface with additional properties needed for this component interface ExtendedEventRequest extends SchemaEventRequest { @@ -12,6 +13,7 @@ interface ExtendedEventRequest extends SchemaEventRequest { email: string; }; invoice_data?: string | any; + invoice_files?: string[]; // Array of invoice file IDs } interface EventRequestDetailsProps { @@ -20,8 +22,404 @@ interface EventRequestDetailsProps { onStatusChange: (id: string, status: "submitted" | "pending" | "completed" | "declined") => Promise; } +// File preview modal component +interface FilePreviewModalProps { + isOpen: boolean; + onClose: () => void; + collectionName: string; + recordId: string; + fileName: string; + displayName: string; +} + +const FilePreviewModal: React.FC = ({ + isOpen, + onClose, + collectionName, + recordId, + fileName, + displayName +}) => { + const [fileUrl, setFileUrl] = useState(''); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [fileType, setFileType] = useState<'image' | 'pdf' | 'other'>('other'); + + useEffect(() => { + const loadFile = async () => { + if (!isOpen) return; + + setIsLoading(true); + setError(null); + + try { + const fileManager = FileManager.getInstance(); + + // Get file URL with token for secure access + const url = await fileManager.getFileUrlWithToken( + collectionName, + recordId, + fileName, + true // Use token for secure access + ); + + setFileUrl(url); + + // Determine file type based on extension + const extension = fileName.split('.').pop()?.toLowerCase() || ''; + if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(extension)) { + setFileType('image'); + } else if (extension === 'pdf') { + setFileType('pdf'); + } else { + setFileType('other'); + } + } catch (err) { + console.error('Error loading file:', err); + setError('Failed to load file. Please try again.'); + } finally { + setIsLoading(false); + } + }; + + loadFile(); + }, [isOpen, collectionName, recordId, fileName]); + + const handleDownload = async () => { + try { + const fileManager = FileManager.getInstance(); + const blob = await fileManager.downloadFile(collectionName, recordId, fileName); + + // Create a download link + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = displayName || fileName; + document.body.appendChild(a); + a.click(); + + // Clean up + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + + toast.success('File downloaded successfully'); + } catch (err) { + console.error('Error downloading file:', err); + toast.error('Failed to download file'); + } + }; + + if (!isOpen) return null; + + return ( +
+ + {/* Header */} +
+

{displayName || fileName}

+
+ + +
+
+ + {/* Content */} +
+ {isLoading ? ( +
+
+

Loading file...

+
+ ) : error ? ( +
+ + + + {error} +
+ ) : ( + <> + {fileType === 'image' && ( +
+ {displayName setError('Failed to load image')} + /> +
+ )} + + {fileType === 'pdf' && ( + + )} + + {fileType === 'other' && ( +
+ + + +

This file type cannot be previewed directly.

+ +
+ )} + + )} +
+
+
+ ); +}; + +// File preview component +const FilePreview: React.FC<{ + fileUrl: string, + fileName: string, + collectionName: string, + recordId: string, + originalFileName: string +}> = ({ + fileUrl, + fileName, + collectionName, + recordId, + originalFileName +}) => { + const [isImage, setIsImage] = useState(false); + const [displayName, setDisplayName] = useState(fileName); + const [error, setError] = useState(false); + const [isModalOpen, setIsModalOpen] = useState(false); + + useEffect(() => { + // Reset error state when fileUrl changes + setError(false); + + // Check if the file is an image based on extension + const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp']; + const extension = fileUrl.split('.').pop()?.toLowerCase() || ''; + setIsImage(imageExtensions.includes(extension)); + + // Try to extract a better file name from the URL + try { + // Extract the file name from the URL path + const urlParts = fileUrl.split('/'); + const lastPart = urlParts[urlParts.length - 1]; + + // If the last part contains a query string, remove it + const cleanName = lastPart.split('?')[0]; + + // Try to decode the URL to get a readable name + const decodedName = decodeURIComponent(cleanName); + + // If we have a valid name that's different from the ID, use it + if (decodedName && decodedName.length > 0 && decodedName !== fileName) { + setDisplayName(decodedName); + } + } catch (e) { + // If anything goes wrong, just use the provided fileName + console.error('Error parsing file name:', e); + } + }, [fileUrl, fileName]); + + // Handle image load error + const handleImageError = () => { + setIsImage(false); + setError(true); + }; + + // Open the file preview modal + const openModal = () => { + setIsModalOpen(true); + }; + + // Close the file preview modal + const closeModal = () => { + setIsModalOpen(false); + }; + + // If there's an error with the file, show a fallback + if (error) { + return ( +
+
+
+ + + + {displayName} +
+ +
+ + {/* File Preview Modal */} + +
+ ); + } + + return ( +
+ {isImage ? ( +
+
+ {displayName} +
+
+ {displayName} + +
+
+ ) : ( +
+
+ + + + {displayName} +
+ +
+ )} + + {/* File Preview Modal */} + +
+ ); + }; + // Separate component for AS Funding tab to isolate any issues const ASFundingTab: React.FC<{ request: ExtendedEventRequest }> = ({ request }) => { + const [isPreviewModalOpen, setIsPreviewModalOpen] = useState(false); + const [selectedFile, setSelectedFile] = useState<{ name: string, displayName: string }>({ name: '', displayName: '' }); + + // Helper function to safely get file extension + const getFileExtension = (filename: string): string => { + if (!filename || typeof filename !== 'string') return ''; + const parts = filename.split('.'); + return parts.length > 1 ? parts[parts.length - 1].toLowerCase() : ''; + }; + + // Helper function to check if a file is an image + const isImageFile = (filename: string): boolean => { + const ext = getFileExtension(filename); + return ['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(ext); + }; + + // Helper function to check if a file is a PDF + const isPdfFile = (filename: string): boolean => { + return getFileExtension(filename) === 'pdf'; + }; + + // Helper function to get a friendly display name from a filename + const getFriendlyFileName = (filename: string, maxLength: number = 20): string => { + if (!filename || typeof filename !== 'string') return 'Unknown File'; + + // Remove any path information if present + let name = filename; + if (name.includes('/')) { + name = name.split('/').pop() || name; + } + + // Remove any URL parameters if present + if (name.includes('?')) { + name = name.split('?')[0]; + } + + // Try to decode the filename if it's URL encoded + try { + name = decodeURIComponent(name); + } catch (e) { + // If decoding fails, just use the original name + } + + // If the name is too long, truncate it and add ellipsis + if (name.length > maxLength) { + // Get the extension + const ext = getFileExtension(name); + if (ext) { + // Keep the extension and truncate the name + const nameWithoutExt = name.substring(0, name.length - ext.length - 1); + const truncatedName = nameWithoutExt.substring(0, maxLength - ext.length - 4); // -4 for the ellipsis and dot + return `${truncatedName}...${ext}`; + } else { + // No extension, just truncate + return `${name.substring(0, maxLength - 3)}...`; + } + } + + return name; + }; + if (!request.as_funding_required) { return (
@@ -47,6 +445,23 @@ const ASFundingTab: React.FC<{ request: ExtendedEventRequest }> = ({ request }) } } + // Check if we have any invoice files + const hasInvoiceFiles = (request.invoice_files && request.invoice_files.length > 0) || request.invoice; + + // Use hardcoded PocketBase URL instead of environment variable + const pocketbaseUrl = "https://pocketbase.ieeeucsd.org"; + + // Open file preview modal + const openFilePreview = (fileName: string, displayName: string) => { + setSelectedFile({ name: fileName, displayName }); + setIsPreviewModalOpen(true); + }; + + // Close file preview modal + const closeFilePreview = () => { + setIsPreviewModalOpen(false); + }; + return (
@@ -61,16 +476,130 @@ const ASFundingTab: React.FC<{ request: ExtendedEventRequest }> = ({ request })
)} + {/* Display invoice files if available */} + {hasInvoiceFiles ? ( +
+

Invoice Files

+
+ {/* Display invoice_files array if available */} + {request.invoice_files && request.invoice_files.map((fileId, index) => { + const extension = getFileExtension(fileId); + const displayName = getFriendlyFileName(fileId, 25); + + return ( +
openFilePreview(fileId, displayName)} + > +
+ {isImageFile(fileId) ? ( + + + + ) : isPdfFile(fileId) ? ( + + + + ) : ( + + + + )} +
+

{displayName}

+

+ {extension ? extension.toUpperCase() : 'FILE'} +

+
+ + + + +
+
+ ); + })} + + {/* Display single invoice file if available */} + {request.invoice && ( +
{ + const invoiceFile = request.invoice || ''; + openFilePreview(invoiceFile, getFriendlyFileName(invoiceFile, 25)); + }} + > +
+ {isImageFile(request.invoice) ? ( + + + + ) : isPdfFile(request.invoice) ? ( + + + + ) : ( + + + + )} +
+

+ {getFriendlyFileName(request.invoice, 25)} +

+

+ {getFileExtension(request.invoice) ? getFileExtension(request.invoice).toUpperCase() : 'FILE'} +

+
+ + + + +
+
+ )} +
+
+ ) : ( +
+ + No invoice files have been uploaded. +
+ )} + + {/* Display invoice data if available */}

Invoice Data

+ + {/* File Preview Modal */} +
); }; // Separate component for invoice table const InvoiceTable: React.FC<{ invoiceData: any }> = ({ invoiceData }) => { + // If no invoice data is provided, show a message + if (!invoiceData) { + return ( +
+ + No invoice data available. +
+ ); + } + try { // Parse invoice data if it's a string let parsedInvoice = null; @@ -120,7 +649,7 @@ const InvoiceTable: React.FC<{ invoiceData: any }> = ({ invoiceData }) => { } // If we still don't have items, check if the object itself looks like an item - if (items.length === 0 && parsedInvoice.item || parsedInvoice.description || parsedInvoice.name) { + if (items.length === 0 && (parsedInvoice.item || parsedInvoice.description || parsedInvoice.name)) { items = [parsedInvoice]; } diff --git a/src/components/dashboard/Officer_EventRequestManagement/EventRequestManagementTable.tsx b/src/components/dashboard/Officer_EventRequestManagement/EventRequestManagementTable.tsx index 96c4fda..4e80aff 100644 --- a/src/components/dashboard/Officer_EventRequestManagement/EventRequestManagementTable.tsx +++ b/src/components/dashboard/Officer_EventRequestManagement/EventRequestManagementTable.tsx @@ -25,6 +25,7 @@ interface ExtendedEventRequest extends SchemaEventRequest { [key: string]: any; }; invoice_data?: any; + invoice_files?: string[]; // Array of invoice file IDs status: "submitted" | "pending" | "completed" | "declined"; }