import React, { useState, useEffect } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; 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'; import { Icon } from "@iconify/react"; import CustomAlert from '../universal/CustomAlert'; import { Authentication } from '../../../scripts/pocketbase/Authentication'; // Extended EventRequest interface with additional properties needed for this component interface ExtendedEventRequest extends SchemaEventRequest { requested_user_expand?: { name: string; email: string; }; expand?: { requested_user?: { id: string; name: string; email: string; emailVisibility?: boolean; [key: string]: any; }; [key: string]: any; }; invoice_data?: string | any; invoice_files?: string[]; // Array of invoice file IDs flyer_files?: string[]; // Add this for PR-related files files?: string[]; // Generic files field room_reservation_needed?: boolean; room_reservation_location?: string; room_reservation_confirmed?: boolean; additional_notes?: string; } interface EventRequestDetailsProps { request: ExtendedEventRequest; onClose: () => void; 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(''); useEffect(() => { if (!isOpen) return; const loadFile = async () => { try { setIsLoading(true); setError(null); // Construct the secure file URL const auth = Authentication.getInstance(); const token = auth.getAuthToken(); const pbUrl = import.meta.env.PUBLIC_POCKETBASE_URL; const secureUrl = `${pbUrl}/api/files/${collectionName}/${recordId}/${fileName}?token=${token}`; setFileUrl(secureUrl); // Determine file type from extension const extension = fileName.split('.').pop()?.toLowerCase() || ''; setFileType(extension); setIsLoading(false); } catch (err) { console.error('Error loading file:', err); setError('Failed to load file. Please try again.'); setIsLoading(false); } }; loadFile(); }, [isOpen, collectionName, recordId, fileName]); const handleDownload = async () => { try { // Create a temporary link element const link = document.createElement('a'); link.href = fileUrl; link.download = displayName || fileName; document.body.appendChild(link); link.click(); document.body.removeChild(link); } catch (err) { console.error('Error downloading file:', err); setError('Failed to download file. Please try again.'); } }; if (!isOpen) return null; return (

{displayName || fileName}

{isLoading ? (
) : error ? (

{error}

) : ( <> {(fileType === 'jpg' || fileType === 'jpeg' || fileType === 'png' || fileType === 'gif') ? (
{displayName setError('Failed to load image.')} />
) : fileType === 'pdf' ? ( ) : (

File Preview Not Available

This file type ({fileType}) cannot be previewed in the browser.

)} )}
); }; // 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 (

AS Funding Required

No

); } // Process invoice data for display let invoiceData = request.invoice_data; // If invoice_data is not available, try to parse itemized_invoice if (!invoiceData && request.itemized_invoice) { try { if (typeof request.itemized_invoice === 'string') { invoiceData = JSON.parse(request.itemized_invoice); } else if (typeof request.itemized_invoice === 'object') { invoiceData = request.itemized_invoice; } } catch (e) { console.error('Failed to parse itemized_invoice:', e); } } // 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 (

AS Funding Required

Yes

{request.food_drinks_being_served && (

Food/Drinks Being Served

Yes

)} {/* 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'}

)}
) : ( )} {/* 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 ( ); } try { // Parse invoice data if it's a string let parsedInvoice = null; if (typeof invoiceData === 'string') { try { parsedInvoice = JSON.parse(invoiceData); } catch (e) { console.error('Failed to parse invoice data string:', e); return ( ); } } else if (typeof invoiceData === 'object' && invoiceData !== null) { parsedInvoice = invoiceData; } // Check if we have valid invoice data if (!parsedInvoice || typeof parsedInvoice !== 'object') { return ( ); } // Extract items array let items = []; if (parsedInvoice.items && Array.isArray(parsedInvoice.items)) { items = parsedInvoice.items; } else if (Array.isArray(parsedInvoice)) { items = parsedInvoice; } else if (parsedInvoice.items && typeof parsedInvoice.items === 'object') { items = [parsedInvoice.items]; // Wrap single item in array } else { // Try to find any array in the object for (const key in parsedInvoice) { if (Array.isArray(parsedInvoice[key])) { items = parsedInvoice[key]; break; } } } // 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)) { items = [parsedInvoice]; } // If we still don't have items, show a message if (items.length === 0) { return ( ); } // Calculate subtotal from items const subtotal = items.reduce((sum: number, item: any) => { const quantity = parseFloat(item?.quantity || 1); const price = parseFloat(item?.unit_price || item?.price || 0); return sum + (quantity * price); }, 0); // Get tax, tip and total const tax = parseFloat(parsedInvoice.tax || parsedInvoice.taxAmount || 0); const tip = parseFloat(parsedInvoice.tip || parsedInvoice.tipAmount || 0); const total = parseFloat(parsedInvoice.total || 0) || (subtotal + tax + tip); // Render the invoice table return (
{items.map((item: any, index: number) => { // Ensure we're not trying to render an object directly const itemName = typeof item?.item === 'object' ? JSON.stringify(item.item) : (item?.item || item?.description || item?.name || 'N/A'); const quantity = parseFloat(item?.quantity || 1); const unitPrice = parseFloat(item?.unit_price || item?.price || 0); const itemTotal = quantity * unitPrice; return ( ); })} {tax > 0 && ( )} {tip > 0 && ( )}
Item Quantity Price Total
{itemName} {quantity} ${unitPrice.toFixed(2)} ${itemTotal.toFixed(2)}
Subtotal: ${subtotal.toFixed(2)}
Tax: ${tax.toFixed(2)}
Tip: ${tip.toFixed(2)}
Total: ${total.toFixed(2)}
{parsedInvoice.vendor && (
Vendor: {parsedInvoice.vendor}
)}
); } catch (error) { console.error('Error rendering invoice table:', error); return ( ); } }; // Now, add a new component for the PR Materials tab const PRMaterialsTab: React.FC<{ request: ExtendedEventRequest }> = ({ request }) => { const [isPreviewModalOpen, setIsPreviewModalOpen] = useState(false); const [selectedFile, setSelectedFile] = useState<{ name: string, displayName: string }>({ name: '', displayName: '' }); // Format date for display const formatDate = (dateString: string) => { if (!dateString) return 'Not specified'; try { const date = new Date(dateString); return date.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); } catch (e) { return dateString; } }; // Use the same utility functions as in the ASFundingTab const getFileExtension = (filename: string): string => { const parts = filename.split('.'); return parts.length > 1 ? parts.pop()?.toLowerCase() || '' : ''; }; const isImageFile = (filename: string): boolean => { const extension = getFileExtension(filename); return ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].includes(extension); }; const isPdfFile = (filename: string): boolean => { return getFileExtension(filename) === 'pdf'; }; const getFriendlyFileName = (filename: string, maxLength: number = 20): string => { const basename = filename.split('/').pop() || filename; if (basename.length <= maxLength) return basename; const extension = getFileExtension(basename); const name = basename.substring(0, basename.length - extension.length - 1); const truncatedName = name.substring(0, maxLength - 3 - extension.length) + '...'; return extension ? `${truncatedName}.${extension}` : truncatedName; }; // Check if we have any PR-related files (flyer_files or related files) const hasFiles = ( request.flyer_files && request.flyer_files.length > 0 || request.files && request.files.length > 0 || request.other_logos && request.other_logos.length > 0 ); // 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 (

Flyers Needed

{request.flyers_needed ? ( Yes ) : ( No )}
{request.flyers_needed && (

Flyer Types

    {request.flyer_type?.map((type, index) => ( {type} ))} {request.other_flyer_type && ( {request.other_flyer_type} )}

Advertising Start Date

{formatDate(request.flyer_advertising_start_date || '')}

Advertising Format

{request.advertising_format || 'Not specified'}

)}

Photography Needed

{request.photography_needed ? ( Yes ) : ( No )}
{/* Logo Requirements Section */}

Required Logos

    {request.required_logos?.map((logo, index) => ( {logo} ))} {(!request.required_logos || request.required_logos.length === 0) && ( No specific logos required )}
{/* Display custom logos if available */} {request.other_logos && request.other_logos.length > 0 && (

Custom Logos

{request.other_logos.map((logoId, index) => { const displayName = getFriendlyFileName(logoId, 15); return ( openFilePreview(logoId, displayName)} initial={{ opacity: 0, y: 5 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.2 + (index * 0.05) }} whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }} >
{isImageFile(logoId) ? ( ) : ( )}

{displayName}

); })}
)}

Additional Requests

{request.flyer_additional_requests || 'None'}

{/* Display PR-related files if available */} {hasFiles && (

Related Files

{/* Display flyer_files if available */} {request.flyer_files && request.flyer_files.map((fileId, index) => { const extension = getFileExtension(fileId); const displayName = getFriendlyFileName(fileId, 25); return ( openFilePreview(fileId, displayName)} initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.2 + (index * 0.05) }} whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }} >
{isImageFile(fileId) ? ( ) : isPdfFile(fileId) ? ( ) : ( )}

{displayName}

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

); })} {/* Display general files if available */} {request.files && request.files.map((fileId, index) => { const extension = getFileExtension(fileId); const displayName = getFriendlyFileName(fileId, 25); return ( openFilePreview(fileId, displayName)} initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.2 + ((request.flyer_files?.length || 0) + index) * 0.05 }} whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }} >
{isImageFile(fileId) ? ( ) : isPdfFile(fileId) ? ( ) : ( )}

{displayName}

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

); })}
)} {/* No files message */} {!hasFiles && ( )} {/* File Preview Modal */}
); }; // Now, update the EventRequestDetails component to use the new PRMaterialsTab const EventRequestDetails = ({ request, onClose, onStatusChange }: EventRequestDetailsProps): React.ReactNode => { const [activeTab, setActiveTab] = useState('details'); const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false); const [newStatus, setNewStatus] = useState<"submitted" | "pending" | "completed" | "declined">("pending"); const [isSubmitting, setIsSubmitting] = useState(false); const [alertInfo, setAlertInfo] = useState<{ show: boolean; type: "success" | "error" | "warning" | "info"; message: string }>({ show: false, type: "info", message: "" }); const formatDate = (dateString: string) => { if (!dateString) return "Not specified"; const date = new Date(dateString); return date.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric' }); }; const getStatusBadge = (status?: "submitted" | "pending" | "completed" | "declined") => { if (!status) return 'badge-warning'; switch (status) { case 'completed': return 'badge-success'; case 'declined': return 'badge-error'; case 'pending': return 'badge-warning'; case 'submitted': return 'badge-info'; default: return 'badge-warning'; } }; const handleStatusChange = async (newStatus: "submitted" | "pending" | "completed" | "declined") => { setNewStatus(newStatus); setIsConfirmModalOpen(true); }; const confirmStatusChange = async () => { setIsSubmitting(true); setAlertInfo({ show: false, type: "info", message: "" }); try { await onStatusChange(request.id, newStatus); setAlertInfo({ show: true, type: "success", message: `Status successfully changed to ${newStatus}.` }); } catch (error) { setAlertInfo({ show: true, type: "error", message: `Failed to update status: ${error}` }); } finally { setIsSubmitting(false); setIsConfirmModalOpen(false); } }; return (
{/* Tabs navigation */}
{request.as_funding_required && ( )} {request.flyers_needed && ( )}
{/* Alert for status updates */} {alertInfo.show && (
setAlertInfo({ ...alertInfo, show: false })} />
)} {/* Status bar */}
Requested by: {request.requested_user_expand?.name || (request.expand?.requested_user?.name) || 'Unknown'} {" - "} {request.requested_user_expand?.email || (request.expand?.requested_user?.email) || 'No email available'}
{request.status?.charAt(0).toUpperCase() + request.status?.slice(1) || 'Pending'} Submitted on {formatDate(request.created)}
{/* Tab content */}
{activeTab === 'details' && (

Event Information

{request.name}

{request.event_description}

{formatDate(request.start_date_time)}

{formatDate(request.end_date_time)}

{request.location}

Requirements & Special Requests

{request.as_funding_required ? 'Yes' : 'No'}

AS Funding Required

{request.flyers_needed ? 'Yes' : 'No'}

PR Materials Needed

{request.room_reservation_needed ? 'Yes' : 'No'}

Room Reservation Needed

{request.room_reservation_needed && (

Room Reservation Details

{request.room_reservation_location || 'Not specified'}

{request.room_reservation_confirmed ? 'Confirmed' : 'Not confirmed'}

)} {request.files && request.files.length > 0 && (

Event Files

{request.files.map((file, index) => ( ))}
)}
)} {activeTab === 'funding' && request.as_funding_required && } {activeTab === 'pr' && request.flyers_needed && }
{/* Confirmation modal */} {isConfirmModalOpen && (

Confirm Status Change

Are you sure you want to change the status to {newStatus}?

)}
); }; export default EventRequestDetails;