import React, { useState, useEffect } from 'react'; import { Icon } from '@iconify/react'; import { Get } from '../../../scripts/pocketbase/Get'; import { Authentication } from '../../../scripts/pocketbase/Authentication'; import { FileManager } from '../../../scripts/pocketbase/FileManager'; import FilePreview from '../universal/FilePreview'; import { motion, AnimatePresence } from 'framer-motion'; import type { ItemizedExpense, Reimbursement, Receipt } from '../../../schemas/pocketbase'; import { DataSyncService } from '../../../scripts/database/DataSyncService'; import { Collections } from '../../../schemas/pocketbase/schema'; import { toast } from 'react-hot-toast'; interface AuditNote { note: string; auditor_id: string; timestamp: string; is_private: boolean; } // Extended Reimbursement interface with component-specific properties interface ReimbursementRequest extends Omit { audit_notes: AuditNote[] | null; } // Extended Receipt interface with component-specific properties interface ReceiptDetails extends Omit { file: string; itemized_expenses: ItemizedExpense[]; audited_by: string[]; created: string; updated: string; } const STATUS_COLORS = { submitted: 'badge-primary', under_review: 'badge-warning', approved: 'badge-success', rejected: 'badge-error', paid: 'badge-success', in_progress: 'badge-info' }; const STATUS_LABELS = { submitted: 'Submitted', under_review: 'Under Review', approved: 'Approved', rejected: 'Rejected', paid: 'Paid', in_progress: 'In Progress' }; const DEPARTMENT_LABELS = { internal: 'Internal', external: 'External', projects: 'Projects', events: 'Events', other: 'Other' }; // Add this after the STATUS_LABELS constant const STATUS_ORDER = ['submitted', 'under_review', 'approved', 'rejected', 'in_progress', 'paid'] as const; const STATUS_ICONS = { submitted: 'heroicons:paper-airplane', under_review: 'heroicons:eye', approved: 'heroicons:check-circle', rejected: 'heroicons:x-circle', in_progress: 'heroicons:clock', paid: 'heroicons:banknotes' } as const; // Add these animation variants const containerVariants = { hidden: { opacity: 0 }, visible: { opacity: 1, transition: { duration: 0.3, when: "beforeChildren", staggerChildren: 0.1 } } }; const itemVariants = { hidden: { opacity: 0, y: 20 }, visible: { opacity: 1, y: 0, transition: { duration: 0.3, type: "spring", stiffness: 100, damping: 15 } } }; export default function ReimbursementList() { const [requests, setRequests] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [selectedRequest, setSelectedRequest] = useState(null); const [showPreview, setShowPreview] = useState(false); const [previewUrl, setPreviewUrl] = useState(''); const [previewFilename, setPreviewFilename] = useState(''); const [selectedReceipt, setSelectedReceipt] = useState(null); const [receiptDetailsMap, setReceiptDetailsMap] = useState>({}); const get = Get.getInstance(); const auth = Authentication.getInstance(); const fileManager = FileManager.getInstance(); useEffect(() => { // console.log('Component mounted'); fetchReimbursements(); }, []); // Add effect to monitor requests state useEffect(() => { // console.log('Requests state updated:', requests); // console.log('Number of requests:', requests.length); }, [requests]); // Add a useEffect to log preview URL and filename changes useEffect(() => { // console.log('Preview URL changed:', previewUrl); // console.log('Preview filename changed:', previewFilename); }, [previewUrl, previewFilename]); // Add a useEffect to log when the preview modal is shown/hidden useEffect(() => { // console.log('Show preview changed:', showPreview); if (showPreview) { // console.log('Selected receipt:', selectedReceipt); } }, [showPreview, selectedReceipt]); const fetchReimbursements = async () => { setLoading(true); setError(''); try { const pb = auth.getPocketBase(); const userId = pb.authStore.model?.id; if (!userId) { throw new Error('User not authenticated'); } // Use DataSyncService to get data from IndexedDB with forced sync const dataSync = DataSyncService.getInstance(); // Sync reimbursements collection await dataSync.syncCollection( Collections.REIMBURSEMENTS, `submitted_by="${userId}"`, '-created', 'audit_notes' ); // Get reimbursements from IndexedDB const reimbursementRecords = await dataSync.getData( Collections.REIMBURSEMENTS, false, // Don't force sync again `submitted_by="${userId}"`, '-created' ); // console.log('Reimbursement records from IndexedDB:', reimbursementRecords); // Process the records const processedRecords = reimbursementRecords.map(record => { // Process audit notes if they exist let auditNotes = null; if (record.audit_notes) { try { // If it's a string, parse it if (typeof record.audit_notes === 'string') { auditNotes = JSON.parse(record.audit_notes); } else { // Otherwise use it directly auditNotes = record.audit_notes; } } catch (e) { // console.error('Error parsing audit notes:', e); } } return { ...record, audit_notes: auditNotes }; }); setRequests(processedRecords); // Fetch receipt details for each reimbursement for (const record of processedRecords) { if (record.receipts && record.receipts.length > 0) { for (const receiptId of record.receipts) { try { // Get receipt from IndexedDB const receiptRecord = await dataSync.getItem( Collections.RECEIPTS, receiptId ); if (receiptRecord) { // Process itemized expenses let itemizedExpenses: ItemizedExpense[] = []; if (receiptRecord.itemized_expenses) { try { if (typeof receiptRecord.itemized_expenses === 'string') { itemizedExpenses = JSON.parse(receiptRecord.itemized_expenses); } else { itemizedExpenses = receiptRecord.itemized_expenses as ItemizedExpense[]; } } catch (e) { // console.error('Error parsing itemized expenses:', e); } } // Add receipt to state setReceiptDetailsMap(prevMap => ({ ...prevMap, [receiptId]: { id: receiptRecord.id, file: receiptRecord.file, created_by: receiptRecord.created_by, date: receiptRecord.date, location_name: receiptRecord.location_name, location_address: receiptRecord.location_address, notes: receiptRecord.notes, tax: receiptRecord.tax, created: receiptRecord.created, updated: receiptRecord.updated, itemized_expenses: itemizedExpenses, audited_by: receiptRecord.audited_by || [] } })); } } catch (e) { // console.error(`Error fetching receipt ${receiptId}:`, e); } } } } } catch (err) { // console.error('Error fetching reimbursements:', err); setError('Failed to load reimbursements. Please try again.'); } finally { setLoading(false); } }; const handlePreviewFile = async (request: ReimbursementRequest, receiptId: string) => { try { // console.log('Previewing file for receipt ID:', receiptId); const pb = auth.getPocketBase(); const fileManager = FileManager.getInstance(); // Set the selected request setSelectedRequest(request); // Check if we already have the receipt details in our map if (receiptDetailsMap[receiptId]) { // console.log('Using cached receipt details'); // Use the cached receipt details setSelectedReceipt(receiptDetailsMap[receiptId]); // Check if the receipt has a file if (!receiptDetailsMap[receiptId].file) { // console.error('Receipt has no file attached'); toast.error('This receipt has no file attached'); setPreviewUrl(''); setPreviewFilename(''); setShowPreview(true); return; } // Get the file URL with token for protected files // console.log('Getting file URL with token'); const url = await fileManager.getFileUrlWithToken( 'receipts', receiptId, receiptDetailsMap[receiptId].file, true // Use token for protected files ); // Check if the URL is empty if (!url) { // console.error('Failed to get file URL: Empty URL returned'); toast.error('Failed to load receipt: Could not generate file URL'); // Still show the preview modal but with empty URL to display the error message setPreviewUrl(''); setPreviewFilename(receiptDetailsMap[receiptId].file || ''); setShowPreview(true); return; } // console.log('Got URL:', url.substring(0, 50) + '...'); // Set the preview URL and filename setPreviewUrl(url); setPreviewFilename(receiptDetailsMap[receiptId].file); // Show the preview modal setShowPreview(true); // Log the current state // console.log('Current state after setting:', { // previewUrl: url, // previewFilename: receiptDetailsMap[receiptId].file, // showPreview: true // }); return; } // If not in the map, get the receipt record using its ID // console.log('Fetching receipt details from server'); const receiptRecord = await pb.collection('receipts').getOne(receiptId, { $autoCancel: false }); if (receiptRecord) { // console.log('Receipt record found:', receiptRecord.id); // console.log('Receipt file:', receiptRecord.file); // Check if the receipt has a file if (!receiptRecord.file) { // console.error('Receipt has no file attached'); toast.error('This receipt has no file attached'); setPreviewUrl(''); setPreviewFilename(''); setShowPreview(true); return; } // Parse the itemized expenses if it's a string const itemizedExpenses = typeof receiptRecord.itemized_expenses === 'string' ? JSON.parse(receiptRecord.itemized_expenses) : receiptRecord.itemized_expenses; const receiptDetails: ReceiptDetails = { id: receiptRecord.id, file: receiptRecord.file, created_by: receiptRecord.created_by, itemized_expenses: itemizedExpenses, tax: receiptRecord.tax, date: receiptRecord.date, location_name: receiptRecord.location_name, location_address: receiptRecord.location_address, notes: receiptRecord.notes || '', audited_by: receiptRecord.audited_by || [], created: receiptRecord.created, updated: receiptRecord.updated }; // Add to the map for future use setReceiptDetailsMap(prevMap => ({ ...prevMap, [receiptId]: receiptDetails })); setSelectedReceipt(receiptDetails); // Get the file URL with token for protected files // console.log('Getting file URL with token for new receipt'); const url = await fileManager.getFileUrlWithToken( 'receipts', receiptRecord.id, receiptRecord.file, true // Use token for protected files ); // Check if the URL is empty if (!url) { // console.error('Failed to get file URL: Empty URL returned'); toast.error('Failed to load receipt: Could not generate file URL'); // Still show the preview modal but with empty URL to display the error message setPreviewUrl(''); setPreviewFilename(receiptRecord.file || ''); setShowPreview(true); return; } // console.log('Got URL:', url.substring(0, 50) + '...'); // Set the preview URL and filename setPreviewUrl(url); setPreviewFilename(receiptRecord.file); // Show the preview modal setShowPreview(true); // Log the current state // console.log('Current state after setting:', { // previewUrl: url, // previewFilename: receiptRecord.file, // showPreview: true // }); } else { throw new Error('Receipt not found'); } } catch (error) { // console.error('Error loading receipt:', error); toast.error('Failed to load receipt. Please try again.'); // Show the preview modal with empty URL to display the error message setPreviewUrl(''); setPreviewFilename(''); setShowPreview(true); } }; const formatDate = (dateString: string) => { return new Date(dateString).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }); }; if (loading) { // console.log('Rendering loading state'); return (

Loading your reimbursements...

); } if (error) { // console.log('Rendering error state:', error); return ( {error} ); } // console.log('Rendering main component. Requests:', requests); // console.log('Requests length:', requests.length); return ( <> {requests.length === 0 ? (

No reimbursement requests

Create a new request to get started

) : ( {requests.map((request, index) => { // console.log('Rendering request:', request); return (

{request.title}

${request.total_amount.toFixed(2)}
{formatDate(request.date_of_purchase)}
{request.audit_notes && request.audit_notes.filter(note => !note.is_private).length > 0 && (
{request.audit_notes.filter(note => !note.is_private).length} Notes
)}
setSelectedRequest(request)} > View Details
{STATUS_ORDER.map((status, index) => { if (status === 'rejected' && request.status !== 'rejected') return null; if (status === 'approved' && request.status === 'rejected') return null; const isActive = STATUS_ORDER.indexOf(request.status) >= STATUS_ORDER.indexOf(status); const isCurrent = request.status === status; return (
{STATUS_LABELS[status]}
); })}
); })} )} {/* Details Modal */} {selectedRequest && (

{selectedRequest.title}

setSelectedRequest(null)} >
{STATUS_LABELS[selectedRequest.status]}
{DEPARTMENT_LABELS[selectedRequest.department]}

${selectedRequest.total_amount.toFixed(2)}

{formatDate(selectedRequest.date_of_purchase)}

{selectedRequest.payment_method}

{selectedRequest.additional_info && (

{selectedRequest.additional_info}

)} {selectedRequest.audit_notes && selectedRequest.audit_notes.filter(note => !note.is_private).length > 0 && (
{selectedRequest.audit_notes .filter(note => !note.is_private) .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()) .map((note, index) => (

{note.note}

{formatDate(note.timestamp)}
))}
)}
{(selectedRequest.receipts || []).map((receiptId, index) => ( handlePreviewFile(selectedRequest, receiptId)} > Receipt #{index + 1} ))}

{formatDate(selectedRequest.created)}

{formatDate(selectedRequest.updated)}

)}
{/* File Preview Modal */} {showPreview && selectedReceipt && (

Receipt Details

{ setShowPreview(false); setSelectedReceipt(null); }} >
{/* Receipt Details */}

{selectedReceipt.location_name}

{selectedReceipt.location_address}

{formatDate(selectedReceipt.date)}

{selectedReceipt.notes && (

{selectedReceipt.notes}

)}
{selectedReceipt.itemized_expenses.map((item, index) => (

{item.description}

{item.category}

${item.amount.toFixed(2)}

))}

${selectedReceipt.tax.toFixed(2)}

${(selectedReceipt.itemized_expenses.reduce((sum, item) => sum + item.amount, 0) + selectedReceipt.tax).toFixed(2)}

{/* File Preview */}

Receipt Image

View Full Size
{previewUrl ? ( ) : (

Receipt Image Not Available

The receipt image could not be loaded. This might be due to permission issues or the file may not exist.

)}
)}
); }