import React, { useState, useEffect } from 'react'; import { Icon } from '@iconify/react'; import FilePreview from '../universal/FilePreview'; import { Get } from '../../../scripts/pocketbase/Get'; import { Update } from '../../../scripts/pocketbase/Update'; import { Authentication } from '../../../scripts/pocketbase/Authentication'; import { FileManager } from '../../../scripts/pocketbase/FileManager'; import { toast } from 'react-hot-toast'; import { motion, AnimatePresence } from 'framer-motion'; import type { Receipt as SchemaReceipt, User, Reimbursement } from '../../../schemas/pocketbase'; // Extended Receipt interface with additional properties needed for this component interface ExtendedReceipt extends Omit { audited_by: string[]; // In schema it's a string, but in this component it's used as string[] auditor_names?: string[]; // Names of auditors } // Extended User interface with additional properties needed for this component interface ExtendedUser extends User { avatar: string; zelle_information: string; } // Extended Reimbursement interface with additional properties needed for this component interface ExtendedReimbursement extends Reimbursement { submitter?: ExtendedUser; } interface FilterOptions { status: string[]; department: string[]; dateRange: 'all' | 'week' | 'month' | 'year'; sortBy: 'date_of_purchase' | 'total_amount' | 'status'; sortOrder: 'asc' | 'desc'; } interface ItemizedExpense { description: string; category: string; amount: number; } export default function ReimbursementManagementPortal() { const [reimbursements, setReimbursements] = useState([]); const [receipts, setReceipts] = useState>({}); const [selectedReimbursement, setSelectedReimbursement] = useState(null); const [selectedReceipt, setSelectedReceipt] = useState(null); const [showReceiptModal, setShowReceiptModal] = useState(false); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [filters, setFilters] = useState({ status: [], department: [], dateRange: 'all', sortBy: 'date_of_purchase', sortOrder: 'desc' }); const [auditNote, setAuditNote] = useState(''); const [loadingStatus, setLoadingStatus] = useState(false); const [expandedReceipts, setExpandedReceipts] = useState>(new Set()); const [auditingReceipt, setAuditingReceipt] = useState(null); const [users, setUsers] = useState>({}); const [showUserProfile, setShowUserProfile] = useState(null); const [auditNotes, setAuditNotes] = useState([]); const userDropdownRef = React.useRef(null); const [currentLogPage, setCurrentLogPage] = useState(1); const [currentNotePage, setCurrentNotePage] = useState(1); const logsPerPage = 5; const [notesPerPage, setNotesPerPage] = useState(5); const [isPrivateNote, setIsPrivateNote] = useState(true); const [showRejectModal, setShowRejectModal] = useState(false); const [rejectReason, setRejectReason] = useState(''); const [rejectingId, setRejectingId] = useState(null); const [receiptUrl, setReceiptUrl] = useState(''); useEffect(() => { const auth = Authentication.getInstance(); if (!auth.isAuthenticated()) { setError('You must be logged in to view reimbursements'); setLoading(false); return; } loadReimbursements(); }, [filters]); // Add click outside handler useEffect(() => { function handleClickOutside(event: MouseEvent) { if (userDropdownRef.current && !userDropdownRef.current.contains(event.target as Node)) { setShowUserProfile(null); } } document.addEventListener('mousedown', handleClickOutside); return () => { document.removeEventListener('mousedown', handleClickOutside); }; }, []); const loadReimbursements = async () => { try { setLoading(true); setError(null); const get = Get.getInstance(); const sort = `${filters.sortOrder === 'desc' ? '-' : ''}${filters.sortBy}`; let filter = ''; if (filters.status.length > 0) { const statusFilter = filters.status.map(s => `status = "${s}"`).join(' || '); filter = `(${statusFilter})`; } if (filters.department.length > 0) { const departmentFilter = filters.department.map(d => `department = "${d}"`).join(' || '); filter = filter ? `${filter} && (${departmentFilter})` : `(${departmentFilter})`; } if (filters.dateRange !== 'all') { const now = new Date(); const cutoff = new Date(); switch (filters.dateRange) { case 'week': cutoff.setDate(now.getDate() - 7); break; case 'month': cutoff.setMonth(now.getMonth() - 1); break; case 'year': cutoff.setFullYear(now.getFullYear() - 1); break; } const dateFilter = `created >= "${cutoff.toISOString()}"`; filter = filter ? `${filter} && ${dateFilter}` : dateFilter; } const records = await get.getAll('reimbursement', filter, sort); // Load user data for submitters const userIds = new Set(records.map(r => r.submitted_by)); const userRecords = await Promise.all( Array.from(userIds).map(async id => { try { return await get.getOne('users', id); } catch (error) { console.error(`Failed to load user ${id}:`, error); return null; } }) ); const validUsers = userRecords.filter((u): u is ExtendedUser => u !== null); const userMap = Object.fromEntries( validUsers.map(user => [user.id, user]) ); setUsers(userMap); // Attach user data to reimbursements const enrichedRecords = records.map(record => ({ ...record, submitter: userMap[record.submitted_by] })); setReimbursements(enrichedRecords); // Load associated receipts const receiptIds = enrichedRecords.flatMap(r => r.receipts || []); if (receiptIds.length > 0) { try { const receiptRecords = await Promise.all( receiptIds.map(async id => { try { const receipt = await get.getOne('receipts', id); // Get auditor names from the users collection if (receipt.audited_by) { // Convert audited_by to array if it's a string const auditorIds = Array.isArray(receipt.audited_by) ? receipt.audited_by : receipt.audited_by ? [receipt.audited_by] : []; if (auditorIds.length > 0) { const auditorUsers = await Promise.all( auditorIds.map(auditorId => get.getOne('users', auditorId) .catch(() => ({ name: 'Unknown User' })) ) ); receipt.auditor_names = auditorUsers.map(user => user.name); // Ensure audited_by is always an array for consistency receipt.audited_by = auditorIds; } } return receipt; } catch (error) { console.error(`Failed to load receipt ${id}:`, error); return null; } }) ); const validReceipts = receiptRecords.filter((r): r is ExtendedReceipt => r !== null); const receiptMap = Object.fromEntries( validReceipts.map(receipt => [receipt.id, receipt]) ); setReceipts(receiptMap); } catch (error: any) { console.error('Error loading receipts:', error); console.error('Error details:', { status: error?.status, message: error?.message, data: error?.data }); toast.error('Failed to load receipts: ' + (error?.message || 'Unknown error')); } } else { // console.log('No receipt IDs found in reimbursements'); setReceipts({}); } } catch (error) { console.error('Error loading reimbursements:', error); toast.error('Failed to load reimbursements. Please try again later.'); setError('Failed to load reimbursements. Please try again later.'); } finally { setLoading(false); } }; // Add helper function to add audit log const addAuditLog = async (reimbursementId: string, action: string, details: Record = {}) => { try { const update = Update.getInstance(); const auth = Authentication.getInstance(); const userId = auth.getUserId(); if (!userId) throw new Error('User not authenticated'); const reimbursement = reimbursements.find(r => r.id === reimbursementId); if (!reimbursement) throw new Error('Reimbursement not found'); // Get current logs let currentLogs = []; try { if (reimbursement.audit_logs) { if (typeof reimbursement.audit_logs === 'string') { currentLogs = JSON.parse(reimbursement.audit_logs); } else { currentLogs = reimbursement.audit_logs; } if (!Array.isArray(currentLogs)) { currentLogs = []; } } } catch (error) { console.error('Error parsing existing audit logs:', error); currentLogs = []; } // Add new log entry currentLogs.push({ action, ...details, auditor_id: userId, timestamp: new Date().toISOString() }); await update.updateFields('reimbursement', reimbursementId, { audit_logs: JSON.stringify(currentLogs) }); } catch (error) { console.error('Error adding audit log:', error); } }; const refreshAuditData = async (reimbursementId: string) => { try { const get = Get.getInstance(); const updatedReimbursement = await get.getOne('reimbursement', reimbursementId); // Get updated user data if needed if (!users[updatedReimbursement.submitted_by]) { const user = await get.getOne('users', updatedReimbursement.submitted_by); setUsers(prev => ({ ...prev, [user.id]: user })); } // Get updated receipt data const updatedReceipts = await Promise.all( updatedReimbursement.receipts.map(async id => { try { const receipt = await get.getOne('receipts', id); // Get updated auditor names if (receipt.audited_by) { // Convert audited_by to array if it's a string const auditorIds = Array.isArray(receipt.audited_by) ? receipt.audited_by : receipt.audited_by ? [receipt.audited_by] : []; if (auditorIds.length > 0) { const auditorUsers = await Promise.all( auditorIds.map(async auditorId => { try { const user = await get.getOne('users', auditorId); // Update users state with any new auditors setUsers(prev => ({ ...prev, [user.id]: user })); return user; } catch { return { name: 'Unknown User' } as ExtendedUser; } }) ); receipt.auditor_names = auditorUsers.map(user => user.name); // Ensure audited_by is always an array for consistency receipt.audited_by = auditorIds; } } return receipt; } catch (error) { console.error(`Failed to load receipt ${id}:`, error); return null; } }) ); const validReceipts = updatedReceipts.filter((r): r is ExtendedReceipt => r !== null); const receiptMap = Object.fromEntries( validReceipts.map(receipt => [receipt.id, receipt]) ); // Update all states setReceipts(prev => ({ ...prev, ...receiptMap })); // Update the reimbursement in the list setReimbursements(prev => prev.map(r => r.id === reimbursementId ? { ...r, ...updatedReimbursement, submitter: users[updatedReimbursement.submitted_by] } : r )); // Update selected reimbursement if it's the one being viewed if (selectedReimbursement?.id === reimbursementId) { setSelectedReimbursement({ ...selectedReimbursement, ...updatedReimbursement, submitter: users[updatedReimbursement.submitted_by] }); } // Reset pagination to first page when data is refreshed setCurrentLogPage(1); setCurrentNotePage(1); } catch (error) { console.error('Error refreshing audit data:', error); toast.error('Failed to refresh audit data'); } }; // Update the updateStatus function const updateStatus = async (id: string, status: 'submitted' | 'under_review' | 'approved' | 'rejected' | 'in_progress' | 'paid') => { try { setLoadingStatus(true); const update = Update.getInstance(); const auth = Authentication.getInstance(); const userId = auth.getUserId(); if (!userId) throw new Error('User not authenticated'); await update.updateFields('reimbursement', id, { status }); // Add audit log for status change await addAuditLog(id, 'status_change', { from: selectedReimbursement?.status, to: status }); toast.success(`Reimbursement ${status} successfully`); await refreshAuditData(id); } catch (error) { console.error('Error updating status:', error); toast.error('Failed to update reimbursement status'); setError('Failed to update reimbursement status. Please try again.'); } finally { setLoadingStatus(false); } }; const toggleReceipt = async (receiptId: string) => { if (expandedReceipts.has(receiptId)) { // If already expanded, collapse it const newSet = new Set(expandedReceipts); newSet.delete(receiptId); setExpandedReceipts(newSet); setSelectedReceipt(null); } else { // If not expanded, expand it const newSet = new Set(expandedReceipts); newSet.add(receiptId); setExpandedReceipts(newSet); // Set the selected receipt const receipt = receipts[receiptId]; if (receipt) { setSelectedReceipt(receipt); // Get the receipt URL and update the state try { const url = await getReceiptUrl(receipt); setReceiptUrl(url); } catch (error) { console.error('Error getting receipt URL:', error); setReceiptUrl(''); } } } }; // Update the auditReceipt function const auditReceipt = async (receiptId: string) => { try { setAuditingReceipt(receiptId); const update = Update.getInstance(); const auth = Authentication.getInstance(); const userId = auth.getUserId(); if (!userId) throw new Error('User not authenticated'); const receipt = receipts[receiptId]; if (!receipt) throw new Error('Receipt not found'); // Get the receipt URL and update the state try { const url = await getReceiptUrl(receipt); setReceiptUrl(url); } catch (error) { console.error('Error getting receipt URL:', error); setReceiptUrl(''); } const updatedAuditors = [...new Set([...receipt.audited_by, userId])]; await update.updateFields('receipts', receiptId, { audited_by: updatedAuditors }); // Add audit log for receipt audit if (selectedReimbursement) { let totalAmount = 0; try { const expenses: ItemizedExpense[] = typeof receipt.itemized_expenses === 'string' ? JSON.parse(receipt.itemized_expenses) : receipt.itemized_expenses; totalAmount = expenses.reduce((sum, item) => sum + item.amount, 0) + receipt.tax; } catch (error) { console.error('Error calculating total amount:', error); } await addAuditLog(selectedReimbursement.id, 'receipt_audit', { receipt_id: receiptId, receipt_name: receipt.location_name, receipt_date: receipt.date, receipt_amount: totalAmount }); await refreshAuditData(selectedReimbursement.id); } // Update local state setReceipts(prev => ({ ...prev, [receiptId]: { ...prev[receiptId], audited_by: updatedAuditors } })); setSelectedReceipt(receipt); setShowReceiptModal(true); toast.success('Receipt audited successfully'); } catch (error) { console.error('Error auditing receipt:', error); toast.error('Failed to audit receipt'); } finally { setAuditingReceipt(null); } }; const canApproveOrReject = (reimbursement: ExtendedReimbursement): boolean => { const auth = Authentication.getInstance(); const userId = auth.getUserId(); if (!userId) return false; // Check if all receipts have been audited by the current user return reimbursement.receipts.every(receiptId => { const receipt = receipts[receiptId]; return receipt && receipt.audited_by.includes(userId); }); }; const getReceiptUrl = async (receipt: ExtendedReceipt): Promise => { try { const fileManager = FileManager.getInstance(); return await fileManager.getFileUrlWithToken( 'receipts', receipt.id, receipt.file, true // Use token for protected files ); } catch (error) { console.error('Error getting receipt URL:', error); return ''; } }; // Add this function to get the user avatar URL const getUserAvatarUrl = (user: ExtendedUser): string => { const auth = Authentication.getInstance(); const pb = auth.getPocketBase(); return pb.files.getURL(user, user.avatar); }; // Update the saveAuditNote function const saveAuditNote = async () => { try { if (!auditNote.trim()) { toast.error('Please enter a note before saving'); return; } if (!selectedReimbursement) return; setLoadingStatus(true); const update = Update.getInstance(); const auth = Authentication.getInstance(); const userId = auth.getUserId(); if (!userId) throw new Error('User not authenticated'); // Parse existing notes or initialize empty array let currentNotes = []; try { if (selectedReimbursement.audit_notes) { if (typeof selectedReimbursement.audit_notes === 'string') { currentNotes = JSON.parse(selectedReimbursement.audit_notes); } else { currentNotes = selectedReimbursement.audit_notes; } if (!Array.isArray(currentNotes)) { currentNotes = []; } } } catch (error) { console.error('Error parsing existing audit notes:', error); currentNotes = []; } // Add new note with privacy setting currentNotes.push({ note: auditNote.trim(), auditor_id: userId, timestamp: new Date().toISOString(), is_private: isPrivateNote }); await update.updateFields('reimbursement', selectedReimbursement.id, { audit_notes: JSON.stringify(currentNotes) }); // Add audit log for note addition await addAuditLog(selectedReimbursement.id, 'note_added', { note_preview: auditNote.length > 50 ? `${auditNote.substring(0, 50)}...` : auditNote, is_private: isPrivateNote }); toast.success('Audit note saved successfully'); setAuditNote(''); setIsPrivateNote(true); await refreshAuditData(selectedReimbursement.id); } catch (error) { console.error('Error saving audit note:', error); toast.error('Failed to save audit note'); } finally { setLoadingStatus(false); } }; // Add this function to get auditor name const getAuditorName = (auditorId: string): string => { return users[auditorId]?.name || 'Unknown User'; }; // Add handleReject function const handleReject = (id: string) => { setRejectingId(id); setRejectReason(''); setShowRejectModal(true); }; // Add submitRejection function const submitRejection = async () => { if (!rejectReason.trim() || !rejectingId) return; try { setLoadingStatus(true); // First update the status await updateStatus(rejectingId, 'rejected'); // Then add the rejection reason as a public note const auth = Authentication.getInstance(); const userId = auth.getUserId(); if (!userId) throw new Error('User not authenticated'); // Get current notes let currentNotes = []; try { const reimbursement = reimbursements.find(r => r.id === rejectingId); if (reimbursement?.audit_notes) { if (typeof reimbursement.audit_notes === 'string') { currentNotes = JSON.parse(reimbursement.audit_notes); } else { currentNotes = reimbursement.audit_notes; } if (!Array.isArray(currentNotes)) { currentNotes = []; } } } catch (error) { console.error('Error parsing existing audit notes:', error); currentNotes = []; } // Add rejection reason as a public note currentNotes.push({ note: `Rejection Reason: ${rejectReason.trim()}`, auditor_id: userId, timestamp: new Date().toISOString(), is_private: false }); const update = Update.getInstance(); await update.updateFields('reimbursement', rejectingId, { audit_notes: JSON.stringify(currentNotes) }); await refreshAuditData(rejectingId); setShowRejectModal(false); setRejectingId(null); setRejectReason(''); toast.success('Reimbursement rejected successfully'); } catch (error) { console.error('Error rejecting reimbursement:', error); toast.error('Failed to reject reimbursement'); } finally { setLoadingStatus(false); } }; if (error) { return ( {error} ); } return (
{/* Left side - List of reimbursements */}

Reimbursement Requests

{reimbursements.length} Total
{filters.status.length > 0 && ( )}
{filters.department.length > 0 && ( )}
{loading ? (

Loading reimbursements...

) : reimbursements.length === 0 ? (

No reimbursements found

) : (
{reimbursements.map((reimbursement, index) => ( setSelectedReimbursement(reimbursement)} >

{reimbursement.title}

{new Date(reimbursement.date_of_purchase).toLocaleDateString()}
{reimbursement.department}
${reimbursement.total_amount.toFixed(2)} {reimbursement.status.replace('_', ' ')}
))}
)}
{/* Right side - Selected reimbursement details */} {selectedReimbursement ? (

{selectedReimbursement.title}

Submitted by: {showUserProfile === selectedReimbursement.submitted_by && (
{selectedReimbursement.submitter?.avatar && ( )}

{selectedReimbursement.submitter?.name}

{selectedReimbursement.submitter?.email}

{selectedReimbursement.submitter?.zelle_information && (

Zelle Information

{selectedReimbursement.submitter.zelle_information}

)}
)}
{selectedReimbursement.status === 'submitted' && ( )} {selectedReimbursement.status === 'under_review' && ( )} {selectedReimbursement.status === 'approved' && ( )} {selectedReimbursement.status === 'in_progress' && ( )} {selectedReimbursement.status !== 'rejected' && selectedReimbursement.status !== 'paid' && ( )}

Date of Purchase

{new Date(selectedReimbursement.date_of_purchase).toLocaleDateString()}

Payment Method

{selectedReimbursement.payment_method}

Department

{selectedReimbursement.department.replace('_', ' ')}

Total Amount

${selectedReimbursement.total_amount.toFixed(2)}

{selectedReimbursement.submitter?.zelle_information && (

Zelle Information

{selectedReimbursement.submitter.zelle_information}

)}
Receipts ({selectedReimbursement.receipts?.length || 0})
{selectedReimbursement.receipts?.length === 0 ? (

No receipts attached

) : ( selectedReimbursement.receipts?.map(receiptId => { const receipt = receipts[receiptId]; if (!receipt) return null; const isExpanded = expandedReceipts.has(receipt.id); return (

{receipt.location_name}

{receipt.location_address}
{new Date(receipt.date).toLocaleDateString()}
{isExpanded && (
{receipt.itemized_expenses && (

Itemized Expenses

{(() => { try { const expenses: ItemizedExpense[] = typeof receipt.itemized_expenses === 'string' ? JSON.parse(receipt.itemized_expenses) : receipt.itemized_expenses; return expenses.map((expense, index) => (

{expense.description}

{expense.category}

${expense.amount.toFixed(2)}
)); } catch (error) { console.error('Error parsing itemized expenses:', error); return

Error loading expenses

; } })()}
)}

Receipt Details

Tax ${receipt.tax.toFixed(2)}
Total ${(() => { try { const expenses: ItemizedExpense[] = typeof receipt.itemized_expenses === 'string' ? JSON.parse(receipt.itemized_expenses) : receipt.itemized_expenses; const subtotal = expenses.reduce((sum, item) => sum + item.amount, 0); return (subtotal + receipt.tax).toFixed(2); } catch (error) { return '0.00'; } })()}
{receipt.notes && (

Notes

{receipt.notes}

)}

Audited by:

{receipt.auditor_names?.length ? (
{receipt.auditor_names.map((name, index) => ( {name} ))}
) : ( Not audited yet )}
)}
); }) )}
{selectedReimbursement.additional_info && ( <>
Additional Information from Member

{selectedReimbursement.additional_info}

)} {selectedReimbursement.audit_logs && ( <>
setExpandedReceipts(prev => { const next = new Set(prev); if (next.has('audit_logs')) { next.delete('audit_logs'); } else { next.add('audit_logs'); } return next; })} >
System Audit Logs
{expandedReceipts.has('audit_logs') && ( {(() => { if (!selectedReimbursement.audit_logs) return null; let currentLogs = []; try { if (selectedReimbursement.audit_logs) { if (typeof selectedReimbursement.audit_logs === 'string') { currentLogs = JSON.parse(selectedReimbursement.audit_logs); } else { currentLogs = selectedReimbursement.audit_logs; } if (!Array.isArray(currentLogs)) { currentLogs = []; } } } catch (error) { console.error('Error parsing existing audit logs:', error); currentLogs = []; } if (currentLogs.length === 0) { return (
No audit logs yet
); } // Sort logs by timestamp in descending order (most recent first) currentLogs.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()); const totalPages = Math.ceil(currentLogs.length / logsPerPage); const startIndex = (currentLogPage - 1) * logsPerPage; const endIndex = startIndex + logsPerPage; const currentPageLogs = currentLogs.slice(startIndex, endIndex); return ( <> {currentPageLogs.map((log: { action: string; from?: string; to?: string; receipt_id?: string; receipt_name?: string; receipt_date?: string; receipt_amount?: number; auditor_id: string; timestamp: string }, index: number) => (
{log.action === 'status_change' ? `Changed to ${log.to?.replace('_', ' ')}` : log.action === 'receipt_audit' ? 'Audited Receipt' : log.action} by {getAuditorName(log.auditor_id)}
{new Date(log.timestamp).toLocaleString()}
{log.action === 'status_change' && log.from && (

Previous status: {log.from.replace('_', ' ')}

)} {log.action === 'receipt_audit' && log.receipt_name && (

Receipt from {log.receipt_name}

)}
))} {totalPages > 1 && (
Page {currentLogPage} of {totalPages}
)} ); })()}
)}
)} {selectedReimbursement.audit_notes && ( <>
setExpandedReceipts(prev => { const next = new Set(prev); if (next.has('audit_notes')) { next.delete('audit_notes'); } else { next.add('audit_notes'); } return next; })} >
Auditor Notes
{expandedReceipts.has('audit_notes') && ( {(() => { if (!selectedReimbursement.audit_notes) return null; let notes = []; try { if (typeof selectedReimbursement.audit_notes === 'string') { notes = JSON.parse(selectedReimbursement.audit_notes); } else { notes = selectedReimbursement.audit_notes; } if (!Array.isArray(notes)) { notes = []; } } catch (error) { console.error('Error parsing audit notes:', error); notes = []; } if (notes.length === 0) { return (
No audit notes yet
); } // Sort notes by timestamp in descending order (most recent first) notes.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()); const totalPages = Math.ceil(notes.length / notesPerPage); const startIndex = (currentNotePage - 1) * notesPerPage; const endIndex = startIndex + notesPerPage; const currentPageNotes = notes.slice(startIndex, endIndex); return ( <> {currentPageNotes.map((note, index) => (
Note by {getAuditorName(note.auditor_id)} {note.is_private && ( Private )}
{new Date(note.timestamp).toLocaleString()}

{note.note}

))} {totalPages > 1 && (
Page {currentNotePage} of {totalPages}
)} ); })()}
)}
)}