import { useState, useEffect } from 'react'; import { motion } from 'framer-motion'; import { Authentication } from '../../../scripts/pocketbase/Authentication'; import { DataSyncService } from '../../../scripts/database/DataSyncService'; import toast from 'react-hot-toast'; import type { EventRequest as SchemaEventRequest } from '../../../schemas/pocketbase/schema'; import { Collections } from '../../../schemas/pocketbase/schema'; // 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; [key: string]: any; }; [key: string]: any; }; invoice_data?: any; invoice_files?: string[]; // Array of invoice file IDs status: "submitted" | "pending" | "completed" | "declined"; declined_reason?: string; // Reason for declining the event request flyers_completed?: boolean; // Track if flyers have been completed by PR team } interface EventRequestManagementTableProps { eventRequests: ExtendedEventRequest[]; onRequestSelect: (request: ExtendedEventRequest) => void; onStatusChange: (id: string, status: "submitted" | "pending" | "completed" | "declined") => Promise; isLoadingUserData?: boolean; } const EventRequestManagementTable = ({ eventRequests: initialEventRequests, onRequestSelect, onStatusChange, isLoadingUserData = false }: EventRequestManagementTableProps) => { const [eventRequests, setEventRequests] = useState(initialEventRequests); const [filteredRequests, setFilteredRequests] = useState(initialEventRequests); const [isRefreshing, setIsRefreshing] = useState(false); const [statusFilter, setStatusFilter] = useState('all'); const [searchTerm, setSearchTerm] = useState(''); const [sortField, setSortField] = useState('created'); const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc'); const dataSync = DataSyncService.getInstance(); // Add state for update modal const [isUpdateModalOpen, setIsUpdateModalOpen] = useState(false); const [requestToUpdate, setRequestToUpdate] = useState(null); // Add state for decline reason modal const [isDeclineModalOpen, setIsDeclineModalOpen] = useState(false); const [declineReason, setDeclineReason] = useState(''); const [requestToDecline, setRequestToDecline] = useState(null); // Refresh event requests const refreshEventRequests = async () => { setIsRefreshing(true); try { const auth = Authentication.getInstance(); // Don't check authentication here - try to fetch anyway // The token might be valid for the API even if isAuthenticated() returns false // console.log("Fetching event requests..."); // Use DataSyncService to get data from IndexedDB with forced sync const updatedRequests = await dataSync.getData( Collections.EVENT_REQUESTS, true, // Force sync '', // No filter '-created', 'requested_user' // This is correct but we need to ensure it's working in the DataSyncService ); // If we still have "Unknown" users, try to fetch them directly const requestsWithUsers = await Promise.all( updatedRequests.map(async (request) => { // If user data is missing, try to fetch it directly if (!request.expand?.requested_user && request.requested_user) { try { const userData = await dataSync.getItem( Collections.USERS, request.requested_user, true // Force sync the user data ); if (userData) { // TypeScript cast to access the properties const typedUserData = userData as unknown as { id: string; name?: string; email?: string; }; // Update the expand object with the user data return { ...request, expand: { ...(request.expand || {}), requested_user: { id: typedUserData.id, name: typedUserData.name || 'Unknown', email: typedUserData.email || 'Unknown' } } } as ExtendedEventRequest; } } catch (error) { console.error(`Error fetching user data for request ${request.id}:`, error); } } return request; }) ) as ExtendedEventRequest[]; // console.log(`Fetched ${updatedRequests.length} event requests`); setEventRequests(requestsWithUsers); applyFilters(requestsWithUsers); } catch (error) { // console.error('Error refreshing event requests:', error); toast.error('Failed to refresh event requests'); } finally { setIsRefreshing(false); } }; // Apply filters and sorting const applyFilters = (requests = eventRequests) => { let filtered = [...requests]; // Apply status filter if (statusFilter !== 'all') { filtered = filtered.filter(request => request.status?.toLowerCase() === statusFilter.toLowerCase() ); } // Apply search filter if (searchTerm) { const term = searchTerm.toLowerCase(); filtered = filtered.filter(request => request.name.toLowerCase().includes(term) || request.location.toLowerCase().includes(term) || request.event_description.toLowerCase().includes(term) || request.expand?.requested_user?.name?.toLowerCase().includes(term) || request.expand?.requested_user?.email?.toLowerCase().includes(term) ); } // Apply sorting filtered.sort((a, b) => { let aValue: any = a[sortField as keyof ExtendedEventRequest]; let bValue: any = b[sortField as keyof ExtendedEventRequest]; // Handle special cases if (sortField === 'requested_user') { aValue = a.expand?.requested_user?.name || ''; bValue = b.expand?.requested_user?.name || ''; } // Handle date fields if (sortField === 'created' || sortField === 'updated' || sortField === 'start_date_time' || sortField === 'end_date_time') { aValue = new Date(aValue || '').getTime(); bValue = new Date(bValue || '').getTime(); } // Compare values based on sort direction if (sortDirection === 'asc') { return aValue > bValue ? 1 : -1; } else { return aValue < bValue ? 1 : -1; } }); setFilteredRequests(filtered); }; // Update event request status const updateEventRequestStatus = async (id: string, status: "submitted" | "pending" | "completed" | "declined", declineReason?: string): Promise => { try { // Find the event request to get its current status and name const eventRequest = eventRequests.find(req => req.id === id); const eventName = eventRequest?.name || 'Event'; const previousStatus = eventRequest?.status; // If declining, update with decline reason if (status === 'declined' && declineReason) { const { Update } = await import('../../../scripts/pocketbase/Update'); const update = Update.getInstance(); await update.updateFields("event_request", id, { status: status, declined_reason: declineReason }); } else { await onStatusChange(id, status); } // Update local state setEventRequests(prev => prev.map(request => request.id === id ? { ...request, status, ...(status === 'declined' && declineReason ? { declined_reason: declineReason } : {}) } : request ) ); setFilteredRequests(prev => prev.map(request => request.id === id ? { ...request, status, ...(status === 'declined' && declineReason ? { declined_reason: declineReason } : {}) } : request ) ); toast.success(`"${eventName}" status updated to ${status}`); // Send email notification for status change try { const { EmailClient } = await import('../../../scripts/email/EmailClient'); const auth = Authentication.getInstance(); const changedByUserId = auth.getUserId(); if (previousStatus && previousStatus !== status) { await EmailClient.notifyEventRequestStatusChange( id, previousStatus, status, changedByUserId || undefined, status === 'declined' ? declineReason : undefined ); console.log('Event request status change notification email sent successfully'); } // Send design team notifications for PR-related actions if (eventRequest?.flyers_needed) { if (status === 'declined') { await EmailClient.notifyDesignTeam(id, 'declined'); console.log('Design team notified of declined PR request'); } } } catch (emailError) { console.error('Failed to send event request status change notification email:', emailError); // Don't show error to user - email failure shouldn't disrupt the main operation } } catch (error) { console.error('Error updating status:', error); toast.error('Failed to update status'); } }; // Update PR status (flyers_completed) const updatePRStatus = async (id: string, completed: boolean): Promise => { try { const { Update } = await import('../../../scripts/pocketbase/Update'); const update = Update.getInstance(); await update.updateField("event_request", id, "flyers_completed", completed); // Find the event request to get its details const eventRequest = eventRequests.find(req => req.id === id); const eventName = eventRequest?.name || 'Event'; // Update local state setEventRequests(prev => prev.map(request => request.id === id ? { ...request, flyers_completed: completed } : request ) ); setFilteredRequests(prev => prev.map(request => request.id === id ? { ...request, flyers_completed: completed } : request ) ); toast.success(`"${eventName}" PR status updated to ${completed ? 'completed' : 'pending'}`); // Send email notification if PR is completed if (completed) { try { const { EmailClient } = await import('../../../scripts/email/EmailClient'); await EmailClient.notifyPRCompleted(id); console.log('PR completion notification email sent successfully'); } catch (emailError) { console.error('Failed to send PR completion notification email:', emailError); // Don't show error to user - email failure shouldn't disrupt the main operation } } } catch (error) { console.error('Error updating PR status:', error); toast.error('Failed to update PR status'); } }; // 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; } }; // Format date and time range for display const formatDateTimeRange = (startDateString: string, endDateString: string) => { if (!startDateString) return 'Not specified'; try { const startDate = new Date(startDateString); const endDate = endDateString ? new Date(endDateString) : null; const startFormatted = startDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); if (endDate && endDate.getTime() !== startDate.getTime()) { // Check if it's the same day const isSameDay = startDate.toDateString() === endDate.toDateString(); if (isSameDay) { // Same day, just show end time const endTime = endDate.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }); return `${startFormatted} - ${endTime}`; } else { // Different day, show full end date const endFormatted = endDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); return `${startFormatted} - ${endFormatted}`; } } return startFormatted; } catch (e) { return startDateString; } }; // Get status badge class based on status 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'; } }; // Helper function to truncate text const truncateText = (text: string, maxLength: number) => { if (!text) return ''; return text.length > maxLength ? `${text.substring(0, maxLength)}...` : text; }; // Helper to get user display info - always show email address const getUserDisplayInfo = (request: ExtendedEventRequest) => { // If we're still loading user data, show loading indicator if (isLoadingUserData) { return { name: request.expand?.requested_user?.name || 'Loading...', email: request.expand?.requested_user?.email || 'Loading...' }; } // First try to get from the expand object if (request.expand?.requested_user) { const user = request.expand.requested_user; const name = user.name || 'Unknown'; // Always show email regardless of emailVisibility const email = user.email || 'Unknown'; return { name, email }; } // Then try the requested_user_expand if (request.requested_user_expand) { const name = request.requested_user_expand.name || 'Unknown'; const email = request.requested_user_expand.email || 'Unknown'; return { name, email }; } // Last fallback return { name: 'Unknown', email: 'Unknown' }; }; // Update openDetailModal to call the prop function const openDetailModal = (request: ExtendedEventRequest) => { onRequestSelect(request); }; // Handle sort change const handleSortChange = (field: string) => { if (sortField === field) { // Toggle direction if same field setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc'); } else { // Set new field and default to descending setSortField(field); setSortDirection('desc'); } }; // Handle decline action with reason prompt const handleDeclineAction = (request: ExtendedEventRequest) => { setRequestToDecline(request); setDeclineReason(''); setIsDeclineModalOpen(true); }; // Confirm decline with reason const confirmDecline = async () => { if (!requestToDecline || !declineReason.trim()) { toast.error('Please provide a reason for declining'); return; } try { await updateEventRequestStatus(requestToDecline.id, 'declined', declineReason); setIsDeclineModalOpen(false); setRequestToDecline(null); setDeclineReason(''); } catch (error) { console.error('Error declining request:', error); toast.error('Failed to decline request'); } }; // Cancel decline action const cancelDecline = () => { setIsDeclineModalOpen(false); setRequestToDecline(null); setDeclineReason(''); }; // Apply filters when filter state changes useEffect(() => { applyFilters(); }, [statusFilter, searchTerm, sortField, sortDirection]); // Check authentication and refresh token if needed useEffect(() => { const checkAuth = async () => { const auth = Authentication.getInstance(); // Check if we're authenticated if (!auth.isAuthenticated()) { // console.log("Authentication check failed - attempting to continue anyway"); // Don't show error or redirect immediately - try to refresh first try { // Try to refresh event requests anyway - the token might be valid await refreshEventRequests(); } catch (err) { // console.error("Failed to refresh after auth check:", err); toast.error("Authentication error. Please log in again."); // Only redirect if refresh fails setTimeout(() => { window.location.href = "/login"; }, 2000); } } else { // console.log("Authentication check passed"); } }; checkAuth(); }, []); // Auto refresh on component mount useEffect(() => { refreshEventRequests(); }, []); // Listen for tab visibility changes and refresh data when tab becomes visible useEffect(() => { const handleTabVisible = () => { // console.log("Tab became visible, refreshing event requests..."); refreshEventRequests(); }; // Add event listener for custom dashboardTabVisible event document.addEventListener("dashboardTabVisible", handleTabVisible); // Clean up event listener on component unmount return () => { document.removeEventListener("dashboardTabVisible", handleTabVisible); }; }, []); if (filteredRequests.length === 0) { return (

No Event Requests Found

{statusFilter !== 'all' || searchTerm ? 'No event requests match your current filters. Try adjusting your search criteria.' : 'There are no event requests in the system yet.'}

{(statusFilter !== 'all' || searchTerm) && ( )}
); } return ( <> {/* Filters and controls */}
setSearchTerm(e.target.value)} />
{filteredRequests.length} {filteredRequests.length === 1 ? 'request' : 'requests'} found
{/* Event requests table */}
{filteredRequests.map((request) => ( ))}
handleSortChange('name')} >
Event Name {sortField === 'name' && ( )}
handleSortChange('start_date_time')} >
Date & Time {sortField === 'start_date_time' && ( )}
handleSortChange('requested_user')} >
Requested By {sortField === 'requested_user' && ( )}
PR Materials handleSortChange('flyers_completed')} >
PR Status {sortField === 'flyers_completed' && ( )}
AS Funding handleSortChange('created')} >
Submitted {sortField === 'created' && ( )}
handleSortChange('status')} >
Status {sortField === 'status' && ( )}
Actions
{truncateText(request.name, 30)}
{formatDateTimeRange(request.start_date_time, request.end_date_time)}
{(() => { const { name, email } = getUserDisplayInfo(request); return (
{name} {email}
); })()}
{request.flyers_needed ? ( Yes ) : ( No )} {request.flyers_needed ? ( { e.stopPropagation(); updatePRStatus(request.id, e.target.checked); }} className="checkbox checkbox-primary" title="Mark PR materials as completed" /> ) : ( )} {request.as_funding_required ? ( Yes ) : ( No )} {formatDate(request.created)} {request.status?.charAt(0).toUpperCase() + request.status?.slice(1) || 'Pending'}
{/* Decline Reason Modal */} {isDeclineModalOpen && (

Decline Event Request

Please provide a reason for declining "{requestToDecline?.name}". This will be sent to the submitter.