added invoice viweing on event request management
This commit is contained in:
parent
1e98df9c53
commit
e45f4a777f
2 changed files with 531 additions and 1 deletions
|
@ -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<void>;
|
||||
}
|
||||
|
||||
// File preview modal component
|
||||
interface FilePreviewModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
collectionName: string;
|
||||
recordId: string;
|
||||
fileName: string;
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
const FilePreviewModal: React.FC<FilePreviewModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
collectionName,
|
||||
recordId,
|
||||
fileName,
|
||||
displayName
|
||||
}) => {
|
||||
const [fileUrl, setFileUrl] = useState<string>('');
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string | null>(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 (
|
||||
<div className="fixed inset-0 bg-black/70 backdrop-blur-sm z-50 flex items-center justify-center p-4 overflow-y-auto">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="bg-base-200 rounded-lg shadow-xl w-full max-w-4xl max-h-[90vh] overflow-hidden flex flex-col"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="bg-base-300 p-4 flex justify-between items-center">
|
||||
<h3 className="text-xl font-bold truncate max-w-[80%]">{displayName || fileName}</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
className="btn btn-sm btn-outline"
|
||||
onClick={handleDownload}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||||
</svg>
|
||||
Download
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm btn-circle"
|
||||
onClick={onClose}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-grow overflow-auto p-4 flex items-center justify-center bg-base-300/30">
|
||||
{isLoading ? (
|
||||
<div className="flex flex-col items-center justify-center p-8">
|
||||
<div className="loading loading-spinner loading-lg"></div>
|
||||
<p className="mt-4">Loading file...</p>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="alert alert-error">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{fileType === 'image' && (
|
||||
<div className="max-h-full max-w-full">
|
||||
<img
|
||||
src={fileUrl}
|
||||
alt={displayName || fileName}
|
||||
className="max-h-[70vh] max-w-full object-contain"
|
||||
onError={() => setError('Failed to load image')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{fileType === 'pdf' && (
|
||||
<iframe
|
||||
src={`${fileUrl}#toolbar=0`}
|
||||
className="w-full h-[70vh]"
|
||||
title={displayName || fileName}
|
||||
></iframe>
|
||||
)}
|
||||
|
||||
{fileType === 'other' && (
|
||||
<div className="flex flex-col items-center justify-center p-8 text-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-16 w-16 mb-4 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<p className="mb-4">This file type cannot be previewed directly.</p>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={handleDownload}
|
||||
>
|
||||
Download File
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 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<boolean>(false);
|
||||
const [displayName, setDisplayName] = useState<string>(fileName);
|
||||
const [error, setError] = useState<boolean>(false);
|
||||
const [isModalOpen, setIsModalOpen] = useState<boolean>(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 (
|
||||
<div className="border rounded-lg overflow-hidden bg-base-300/30 p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-warning" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
<span className="text-sm truncate max-w-[80%]">{displayName}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={openModal}
|
||||
className="btn btn-xs btn-ghost"
|
||||
>
|
||||
View
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* File Preview Modal */}
|
||||
<FilePreviewModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={closeModal}
|
||||
collectionName={collectionName}
|
||||
recordId={recordId}
|
||||
fileName={originalFileName}
|
||||
displayName={displayName}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border rounded-lg overflow-hidden bg-base-300/30">
|
||||
{isImage ? (
|
||||
<div className="flex flex-col">
|
||||
<div className="relative aspect-video cursor-pointer" onClick={openModal}>
|
||||
<img
|
||||
src={fileUrl}
|
||||
alt={displayName}
|
||||
className="object-contain w-full h-full"
|
||||
onError={handleImageError} // Fallback if not an image
|
||||
/>
|
||||
</div>
|
||||
<div className="p-2 flex justify-between items-center">
|
||||
<span className="text-sm truncate max-w-[80%]" title={displayName}>{displayName}</span>
|
||||
<button
|
||||
onClick={openModal}
|
||||
className="btn btn-xs btn-ghost"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-3 flex justify-between items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<span className="text-sm truncate max-w-[80%]" title={displayName}>{displayName}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={openModal}
|
||||
className="btn btn-xs btn-ghost"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* File Preview Modal */}
|
||||
<FilePreviewModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={closeModal}
|
||||
collectionName={collectionName}
|
||||
recordId={recordId}
|
||||
fileName={originalFileName}
|
||||
displayName={displayName}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Separate component for AS Funding tab to isolate any issues
|
||||
const ASFundingTab: React.FC<{ request: ExtendedEventRequest }> = ({ request }) => {
|
||||
const [isPreviewModalOpen, setIsPreviewModalOpen] = useState<boolean>(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 (
|
||||
<div>
|
||||
|
@ -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 (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
|
@ -61,16 +476,130 @@ const ASFundingTab: React.FC<{ request: ExtendedEventRequest }> = ({ request })
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Display invoice files if available */}
|
||||
{hasInvoiceFiles ? (
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-400 mb-1">Invoice Files</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mt-2">
|
||||
{/* 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 (
|
||||
<div
|
||||
key={`file-${index}`}
|
||||
className="border rounded-lg overflow-hidden bg-base-300/30 hover:bg-base-300/50 transition-colors cursor-pointer"
|
||||
onClick={() => openFilePreview(fileId, displayName)}
|
||||
>
|
||||
<div className="p-4 flex items-center gap-3">
|
||||
{isImageFile(fileId) ? (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
) : isPdfFile(fileId) ? (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8 text-error" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8 text-secondary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
)}
|
||||
<div className="flex-grow">
|
||||
<p className="font-medium truncate" title={fileId}>{displayName}</p>
|
||||
<p className="text-xs text-gray-400">
|
||||
{extension ? extension.toUpperCase() : 'FILE'}
|
||||
</p>
|
||||
</div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Display single invoice file if available */}
|
||||
{request.invoice && (
|
||||
<div
|
||||
key="invoice"
|
||||
className="border rounded-lg overflow-hidden bg-base-300/30 hover:bg-base-300/50 transition-colors cursor-pointer"
|
||||
onClick={() => {
|
||||
const invoiceFile = request.invoice || '';
|
||||
openFilePreview(invoiceFile, getFriendlyFileName(invoiceFile, 25));
|
||||
}}
|
||||
>
|
||||
<div className="p-4 flex items-center gap-3">
|
||||
{isImageFile(request.invoice) ? (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
) : isPdfFile(request.invoice) ? (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8 text-error" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8 text-secondary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
)}
|
||||
<div className="flex-grow">
|
||||
<p className="font-medium truncate" title={request.invoice}>
|
||||
{getFriendlyFileName(request.invoice, 25)}
|
||||
</p>
|
||||
<p className="text-xs text-gray-400">
|
||||
{getFileExtension(request.invoice) ? getFileExtension(request.invoice).toUpperCase() : 'FILE'}
|
||||
</p>
|
||||
</div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="alert alert-info">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" className="stroke-current shrink-0 w-6 h-6"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||
<span>No invoice files have been uploaded.</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Display invoice data if available */}
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-400 mb-1">Invoice Data</h4>
|
||||
<InvoiceTable invoiceData={invoiceData} />
|
||||
</div>
|
||||
|
||||
{/* File Preview Modal */}
|
||||
<FilePreviewModal
|
||||
isOpen={isPreviewModalOpen}
|
||||
onClose={closeFilePreview}
|
||||
collectionName={Collections.EVENT_REQUESTS}
|
||||
recordId={request.id}
|
||||
fileName={selectedFile.name}
|
||||
displayName={selectedFile.displayName}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Separate component for invoice table
|
||||
const InvoiceTable: React.FC<{ invoiceData: any }> = ({ invoiceData }) => {
|
||||
// If no invoice data is provided, show a message
|
||||
if (!invoiceData) {
|
||||
return (
|
||||
<div className="alert alert-info">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" className="stroke-current shrink-0 w-6 h-6"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||
<span>No invoice data available.</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue