diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..5e901d0
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,7 @@
+{
+ "workbench.colorCustomizations": {
+ "activityBar.background": "#221489",
+ "titleBar.activeBackground": "#301DC0",
+ "titleBar.activeForeground": "#F9F9FE"
+ }
+}
\ No newline at end of file
diff --git a/src/components/dashboard/reimbursement/ReimbursementList.tsx b/src/components/dashboard/reimbursement/ReimbursementList.tsx
index baa9ce7..5654ab8 100644
--- a/src/components/dashboard/reimbursement/ReimbursementList.tsx
+++ b/src/components/dashboard/reimbursement/ReimbursementList.tsx
@@ -8,6 +8,7 @@ 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;
@@ -121,6 +122,20 @@ export default function ReimbursementList() {
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('');
@@ -241,27 +256,88 @@ export default function ReimbursementList() {
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]);
- // Get the file URL using the PocketBase URL and collection info
- const url = `${pb.baseUrl}/api/files/receipts/${receiptId}/${receiptDetailsMap[receiptId].file}`;
+ // 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)
@@ -290,16 +366,51 @@ export default function ReimbursementList() {
setSelectedReceipt(receiptDetails);
- // Get the file URL using the PocketBase URL and collection info
- const url = `${pb.baseUrl}/api/files/receipts/${receiptRecord.id}/${receiptRecord.file}`;
+ // 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);
}
};
@@ -705,11 +816,25 @@ export default function ReimbursementList() {
-
+ {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.
+
+
+
+ )}
diff --git a/src/components/dashboard/reimbursement/ReimbursementManagementPortal.tsx b/src/components/dashboard/reimbursement/ReimbursementManagementPortal.tsx
index 04ea029..f100688 100644
--- a/src/components/dashboard/reimbursement/ReimbursementManagementPortal.tsx
+++ b/src/components/dashboard/reimbursement/ReimbursementManagementPortal.tsx
@@ -4,6 +4,7 @@ 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';
@@ -70,6 +71,7 @@ export default function ReimbursementManagementPortal() {
const [showRejectModal, setShowRejectModal] = useState(false);
const [rejectReason, setRejectReason] = useState('');
const [rejectingId, setRejectingId] = useState(null);
+ const [receiptUrl, setReceiptUrl] = useState('');
useEffect(() => {
const auth = Authentication.getInstance();
@@ -379,16 +381,34 @@ export default function ReimbursementManagementPortal() {
}
};
- const toggleReceipt = (receiptId: string) => {
- setExpandedReceipts(prev => {
- const next = new Set(prev);
- if (next.has(receiptId)) {
- next.delete(receiptId);
- } else {
- next.add(receiptId);
+ 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('');
+ }
}
- return next;
- });
+ }
};
// Update the auditReceipt function
@@ -404,6 +424,15 @@ export default function ReimbursementManagementPortal() {
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, {
@@ -441,6 +470,8 @@ export default function ReimbursementManagementPortal() {
}
}));
+ setSelectedReceipt(receipt);
+ setShowReceiptModal(true);
toast.success('Receipt audited successfully');
} catch (error) {
console.error('Error auditing receipt:', error);
@@ -463,10 +494,15 @@ export default function ReimbursementManagementPortal() {
});
};
- const getReceiptUrl = (receipt: ExtendedReceipt): string => {
+ const getReceiptUrl = async (receipt: ExtendedReceipt): Promise => {
try {
- const pb = Authentication.getInstance().getPocketBase();
- return pb.files.getURL(receipt, receipt.file);
+ 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 '';
@@ -1648,7 +1684,7 @@ export default function ReimbursementManagementPortal() {
diff --git a/src/components/dashboard/universal/FilePreview.tsx b/src/components/dashboard/universal/FilePreview.tsx
index 8f5fafd..135e7ae 100644
--- a/src/components/dashboard/universal/FilePreview.tsx
+++ b/src/components/dashboard/universal/FilePreview.tsx
@@ -2,6 +2,7 @@ import { useEffect, useState, useCallback, useMemo } from 'react';
import { Icon } from "@iconify/react";
import hljs from 'highlight.js';
import 'highlight.js/styles/github-dark.css';
+import { Authentication } from '../../../scripts/pocketbase/Authentication';
// Cache for file content
const contentCache = new Map();
@@ -36,6 +37,19 @@ export default function FilePreview({ url: initialUrl = '', filename: initialFil
return `${truncatedName}...${extension ? `.${extension}` : ''}`;
}, [filename]);
+ // Update URL and filename when props change
+ useEffect(() => {
+ console.log('FilePreview props changed:', { initialUrl, initialFilename });
+ if (initialUrl !== url) {
+ console.log('URL changed from props:', initialUrl);
+ setUrl(initialUrl);
+ }
+ if (initialFilename !== filename) {
+ console.log('Filename changed from props:', initialFilename);
+ setFilename(initialFilename);
+ }
+ }, [initialUrl, initialFilename, url, filename]);
+
// Intersection Observer callback
useEffect(() => {
const observer = new IntersectionObserver(
@@ -55,7 +69,20 @@ export default function FilePreview({ url: initialUrl = '', filename: initialFil
}, []);
const loadContent = useCallback(async () => {
- if (!url || !filename) return;
+ if (!url) {
+ // Don't log a warning if URL is empty during initial component mount
+ // This is a normal state before the URL is set
+ setError('No file URL provided');
+ setLoading(false);
+ return;
+ }
+
+ if (!filename) {
+ console.warn('Cannot load content: Filename is empty');
+ setError('No filename provided');
+ setLoading(false);
+ return;
+ }
console.log('Loading content for:', { url, filename });
setLoading(true);
@@ -72,9 +99,61 @@ export default function FilePreview({ url: initialUrl = '', filename: initialFil
return;
}
+ // Check if it's likely an image based on filename extension
+ const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
+ const fileExtension = filename.split('.').pop()?.toLowerCase() || '';
+ const isProbablyImage = imageExtensions.includes(fileExtension);
+
+ if (isProbablyImage) {
+ // Try loading as an image first to bypass CORS issues
+ try {
+ console.log('Trying to load as image:', url);
+ const img = new Image();
+ img.crossOrigin = 'anonymous'; // Try anonymous mode first
+
+ // Create a promise to handle image loading
+ const imageLoaded = new Promise((resolve, reject) => {
+ img.onload = () => resolve('image');
+ img.onerror = (e) => reject(new Error('Failed to load image'));
+ });
+
+ img.src = url;
+
+ // Wait for image to load
+ await imageLoaded;
+
+ // If we get here, image loaded successfully
+ setContent('image');
+ setFileType('image/' + fileExtension);
+
+ // Cache the content
+ contentCache.set(cacheKey, {
+ content: 'image',
+ fileType: 'image/' + fileExtension,
+ timestamp: Date.now()
+ });
+
+ setLoading(false);
+ return;
+ } catch (imgError) {
+ console.warn('Failed to load as image, falling back to fetch:', imgError);
+ // Continue to fetch method
+ }
+ }
+
try {
- console.log('Fetching file...');
- const response = await fetch(url);
+ console.log('Fetching file from URL:', url);
+ const response = await fetch(url, {
+ headers: {
+ 'Cache-Control': 'no-cache', // Bypass cache
+ }
+ });
+
+ if (!response.ok) {
+ console.error('File fetch failed with status:', response.status, response.statusText);
+ throw new Error(`Failed to fetch file: ${response.status} ${response.statusText}`);
+ }
+
const contentType = response.headers.get('content-type');
console.log('Received content type:', contentType);
setFileType(contentType);
@@ -112,13 +191,15 @@ export default function FilePreview({ url: initialUrl = '', filename: initialFil
}, [url, filename]);
useEffect(() => {
- if (isVisible || !isModal) { // Load content immediately if not in modal
+ // Only attempt to load content if URL is not empty
+ if ((isVisible || !isModal) && url) {
loadContent();
}
- }, [isVisible, loadContent, isModal]);
+ }, [isVisible, loadContent, isModal, url]);
useEffect(() => {
- console.log('FilePreview component mounted');
+ console.log('FilePreview component mounted or updated with URL:', url);
+ console.log('Filename:', filename);
if (isModal) {
const handleStateChange = (event: CustomEvent<{ url: string; filename: string }>) => {
@@ -145,6 +226,14 @@ export default function FilePreview({ url: initialUrl = '', filename: initialFil
}
}, [isModal, initialUrl, initialFilename]);
+ // Add a new effect to handle URL changes
+ useEffect(() => {
+ if (url && isVisible) {
+ console.log('URL changed, loading content:', url);
+ loadContent();
+ }
+ }, [url, isVisible, loadContent]);
+
const handleDownload = async () => {
try {
const response = await fetch(url);
@@ -250,44 +339,26 @@ export default function FilePreview({ url: initialUrl = '', filename: initialFil
}
try {
- return hljs.highlight(code, { language }).value;
+ // Use highlight.js to highlight the code
+ const highlighted = hljs.highlight(code, { language }).value;
+ return highlighted;
} catch (error) {
- console.warn(`Failed to highlight code for language ${language}:`, error);
- return code;
+ console.warn(`Failed to highlight code as ${language}, falling back to plaintext`);
+ return hljs.highlight(code, { language: 'plaintext' }).value;
}
}, []);
const formatCodeWithLineNumbers = useCallback((code: string, language: string) => {
- // Special handling for CSV files
- if (language === 'csv') {
- return renderCSVTable(code);
- }
+ const highlighted = highlightCode(code, language);
+ const lines = highlighted.split('\n').slice(0, visibleLines);
- const lines = code.split('\n');
- const totalLines = lines.length;
- const linesToShow = Math.min(visibleLines, totalLines);
-
- let formattedCode = lines
- .slice(0, linesToShow)
- .map((line, index) => {
- const lineNumber = index + 1;
- const highlightedLine = highlightCode(line, language);
- return `
-
${lineNumber}
-
${highlightedLine || ' '}
-
`;
- })
- .join('');
-
- if (linesToShow < totalLines) {
- formattedCode += `
-
-
... ${totalLines - linesToShow} more lines
-
`;
- }
-
- return formattedCode;
- }, [highlightCode, visibleLines, renderCSVTable]);
+ return lines.map((line, i) =>
+ `
+
${i + 1}
+
${line || ' '}
+
`
+ ).join('');
+ }, [visibleLines, highlightCode]);
const handleShowMore = useCallback(() => {
setVisibleLines(prev => Math.min(prev + CHUNK_SIZE, content?.split('\n').length || 0));
@@ -297,84 +368,104 @@ export default function FilePreview({ url: initialUrl = '', filename: initialFil
setVisibleLines(INITIAL_LINES_TO_SHOW);
}, []);
+ // If URL is empty, show a message
+ if (!url) {
+ return (
+
+
+
+
No File URL Provided
+
Please check if the file exists or if you have the necessary permissions.
+
+
+ );
+ }
+
return (
- {!loading && !error && content === 'image' && (
-
-
-
-
{truncatedFilename}
- {fileType && (
-
{fileType.split('/')[1]}
- )}
+
+
+ {truncatedFilename}
+ {fileType && (
+ {fileType.split('/')[1]}
+ )}
+
+
+
+ Download
+
+
+
+
+ {loading && (
+
+
+
+ )}
+
+ {error && (
+
+
+
+
+
+
Preview Unavailable
+
{error}
- Download
+ Download File Instead
+
+
+ Try Again
+ )}
+
+ {!loading && !error && content === 'image' && (
{
+ console.error('Image failed to load:', e);
+ setError('Failed to load image. This might be due to permission issues or the file may not exist.');
+
+ // Log additional details
+ console.log('Image URL that failed:', url);
+ console.log('Current auth status:',
+ Authentication.getInstance().isAuthenticated() ? 'Authenticated' : 'Not authenticated'
+ );
+ }}
/>
-
- )}
+ )}
- {!loading && !error && content === 'video' && (
-
-
-
- {truncatedFilename}
- {fileType && (
- {fileType.split('/')[1]}
- )}
-
-
-
- Download
-
-
+ {!loading && !error && content === 'video' && (
-
+
Your browser does not support the video tag.
-
- )}
+ )}
- {!loading && !error && content === 'pdf' && (
-
-
-
- {truncatedFilename}
- {fileType && (
- {fileType.split('/')[1]}
- )}
-
-
-
- Download
-
-
+ {!loading && !error && content === 'pdf' && (
-
- )}
+ )}
- {!loading && !error && content && !['image', 'video', 'pdf'].includes(content) && (
-
-
-
- {truncatedFilename}
- {fileType && (
- {fileType.split('/')[1]}
- )}
-
-
- {content && content.split('\n').length > visibleLines && (
-
- Show More
-
- )}
- {visibleLines > INITIAL_LINES_TO_SHOW && (
-
- Show Less
-
- )}
-
-
- Download
-
-
-
+ {!loading && !error && content && !['image', 'video', 'pdf'].includes(content) && (
-
+ {filename.toLowerCase().endsWith('.csv') ? (
+
+ ) : (
+
+
+
+ )}
-
- )}
-
- {loading && (
-
-
-
- )}
-
- {error && (
-
-
-
-
-
-
Preview Unavailable
-
{error}
-
-
-
- Download File Instead
-
-
- )}
+ )}
+
);
}
\ No newline at end of file
diff --git a/src/scripts/pocketbase/FileManager.ts b/src/scripts/pocketbase/FileManager.ts
index 9ee4a4e..eb4524c 100644
--- a/src/scripts/pocketbase/FileManager.ts
+++ b/src/scripts/pocketbase/FileManager.ts
@@ -30,7 +30,7 @@ export class FileManager {
collectionName: string,
recordId: string,
field: string,
- file: File
+ file: File,
): Promise
{
if (!this.auth.isAuthenticated()) {
throw new Error("User must be authenticated to upload files");
@@ -42,7 +42,9 @@ export class FileManager {
const formData = new FormData();
formData.append(field, file);
- const result = await pb.collection(collectionName).update(recordId, formData);
+ const result = await pb
+ .collection(collectionName)
+ .update(recordId, formData);
return result;
} catch (err) {
console.error(`Failed to upload file to ${collectionName}:`, err);
@@ -64,7 +66,7 @@ export class FileManager {
collectionName: string,
recordId: string,
field: string,
- files: File[]
+ files: File[],
): Promise {
if (!this.auth.isAuthenticated()) {
throw new Error("User must be authenticated to upload files");
@@ -73,14 +75,16 @@ export class FileManager {
try {
this.auth.setUpdating(true);
const pb = this.auth.getPocketBase();
-
+
const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB per file limit
const MAX_BATCH_SIZE = 25 * 1024 * 1024; // 25MB per batch
-
+
// Validate file sizes first
for (const file of files) {
if (file.size > MAX_FILE_SIZE) {
- throw new Error(`File ${file.name} is too large. Maximum size is 50MB.`);
+ throw new Error(
+ `File ${file.name} is too large. Maximum size is 50MB.`,
+ );
}
}
@@ -88,10 +92,12 @@ export class FileManager {
let existingFiles: string[] = [];
if (recordId) {
try {
- const record = await pb.collection(collectionName).getOne(recordId);
+ const record = await pb
+ .collection(collectionName)
+ .getOne(recordId);
existingFiles = (record as any)[field] || [];
} catch (error) {
- console.warn('Failed to fetch existing record:', error);
+ console.warn("Failed to fetch existing record:", error);
}
}
@@ -103,10 +109,10 @@ export class FileManager {
// Process each file
for (const file of files) {
let processedFile = file;
-
+
try {
// Try to compress image files if needed
- if (file.type.startsWith('image/')) {
+ if (file.type.startsWith("image/")) {
processedFile = await this.compressImageIfNeeded(file, 50); // 50MB max size
}
} catch (error) {
@@ -118,7 +124,12 @@ export class FileManager {
if (currentBatchSize + processedFile.size > MAX_BATCH_SIZE) {
// Upload current batch
if (currentBatch.length > 0) {
- await this.uploadBatch(collectionName, recordId, field, currentBatch);
+ await this.uploadBatch(
+ collectionName,
+ recordId,
+ field,
+ currentBatch,
+ );
allProcessedFiles.push(...currentBatch);
}
// Reset batch
@@ -138,7 +149,9 @@ export class FileManager {
}
// Get the final record state
- const finalRecord = await pb.collection(collectionName).getOne(recordId);
+ const finalRecord = await pb
+ .collection(collectionName)
+ .getOne(recordId);
return finalRecord;
} catch (err) {
console.error(`Failed to upload files to ${collectionName}:`, err);
@@ -156,7 +169,7 @@ export class FileManager {
collectionName: string,
recordId: string,
field: string,
- files: File[]
+ files: File[],
): Promise {
const pb = this.auth.getPocketBase();
const formData = new FormData();
@@ -170,7 +183,9 @@ export class FileManager {
await pb.collection(collectionName).update(recordId, formData);
} catch (error: any) {
if (error.status === 413) {
- throw new Error(`Upload failed: Batch size too large. Please try uploading smaller files.`);
+ throw new Error(
+ `Upload failed: Batch size too large. Please try uploading smaller files.`,
+ );
}
throw error;
}
@@ -188,7 +203,7 @@ export class FileManager {
collectionName: string,
recordId: string,
field: string,
- files: File[]
+ files: File[],
): Promise {
if (!this.auth.isAuthenticated()) {
throw new Error("User must be authenticated to upload files");
@@ -197,10 +212,10 @@ export class FileManager {
try {
this.auth.setUpdating(true);
const pb = this.auth.getPocketBase();
-
+
// First, get the current record to check existing files
const record = await pb.collection(collectionName).getOne(recordId);
-
+
// Create FormData with existing files
const formData = new FormData();
@@ -210,7 +225,9 @@ export class FileManager {
// For each existing file, we need to fetch it and add it to the FormData
for (const existingFile of existingFiles) {
try {
- const response = await fetch(this.getFileUrl(collectionName, recordId, existingFile));
+ const response = await fetch(
+ this.getFileUrl(collectionName, recordId, existingFile),
+ );
const blob = await response.blob();
const file = new File([blob], existingFile, { type: blob.type });
formData.append(field, file);
@@ -218,13 +235,15 @@ export class FileManager {
console.warn(`Failed to fetch existing file ${existingFile}:`, error);
}
}
-
+
// Append new files
- files.forEach(file => {
+ files.forEach((file) => {
formData.append(field, file);
});
- const result = await pb.collection(collectionName).update(recordId, formData);
+ const result = await pb
+ .collection(collectionName)
+ .update(recordId, formData);
return result;
} catch (err) {
console.error(`Failed to append files to ${collectionName}:`, err);
@@ -244,12 +263,13 @@ export class FileManager {
public getFileUrl(
collectionName: string,
recordId: string,
- filename: string
+ filename: string,
): string {
const pb = this.auth.getPocketBase();
- const token = pb.authStore.token;
- const url = `${pb.baseUrl}/api/files/${collectionName}/${recordId}/${filename}`;
- return url;
+ return pb.files.getURL(
+ { id: recordId, collectionId: collectionName },
+ filename,
+ );
}
/**
@@ -262,7 +282,7 @@ export class FileManager {
public async deleteFile(
collectionName: string,
recordId: string,
- field: string
+ field: string,
): Promise {
if (!this.auth.isAuthenticated()) {
throw new Error("User must be authenticated to delete files");
@@ -272,7 +292,9 @@ export class FileManager {
this.auth.setUpdating(true);
const pb = this.auth.getPocketBase();
const data = { [field]: null };
- const result = await pb.collection(collectionName).update(recordId, data);
+ const result = await pb
+ .collection(collectionName)
+ .update(recordId, data);
return result;
} catch (err) {
console.error(`Failed to delete file from ${collectionName}:`, err);
@@ -292,7 +314,7 @@ export class FileManager {
public async downloadFile(
collectionName: string,
recordId: string,
- filename: string
+ filename: string,
): Promise {
if (!this.auth.isAuthenticated()) {
throw new Error("User must be authenticated to download files");
@@ -302,11 +324,11 @@ export class FileManager {
this.auth.setUpdating(true);
const url = this.getFileUrl(collectionName, recordId, filename);
const response = await fetch(url);
-
+
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
-
+
const result = await response.blob();
return result;
} catch (err) {
@@ -327,7 +349,7 @@ export class FileManager {
public async getFiles(
collectionName: string,
recordId: string,
- field: string
+ field: string,
): Promise {
if (!this.auth.isAuthenticated()) {
throw new Error("User must be authenticated to get files");
@@ -336,18 +358,18 @@ export class FileManager {
try {
this.auth.setUpdating(true);
const pb = this.auth.getPocketBase();
-
+
// Get the record to retrieve the filenames
const record = await pb.collection(collectionName).getOne(recordId);
-
+
// Get the filenames from the specified field
const filenames = record[field] || [];
-
+
// Convert filenames to URLs
- const fileUrls = filenames.map((filename: string) =>
- this.getFileUrl(collectionName, recordId, filename)
+ const fileUrls = filenames.map((filename: string) =>
+ this.getFileUrl(collectionName, recordId, filename),
);
-
+
return fileUrls;
} catch (err) {
console.error(`Failed to get files from ${collectionName}:`, err);
@@ -363,8 +385,11 @@ export class FileManager {
* @param maxSizeInMB Maximum size in MB
* @returns Promise The compressed file
*/
- public async compressImageIfNeeded(file: File, maxSizeInMB: number = 50): Promise {
- if (!file.type.startsWith('image/')) {
+ public async compressImageIfNeeded(
+ file: File,
+ maxSizeInMB: number = 50,
+ ): Promise {
+ if (!file.type.startsWith("image/")) {
return file;
}
@@ -379,12 +404,12 @@ export class FileManager {
reader.onload = (e) => {
const img = new Image();
img.src = e.target?.result as string;
-
+
img.onload = () => {
- const canvas = document.createElement('canvas');
+ const canvas = document.createElement("canvas");
let width = img.width;
let height = img.height;
-
+
// Calculate new dimensions while maintaining aspect ratio
const maxDimension = 3840; // Higher quality for larger files
if (width > height && width > maxDimension) {
@@ -397,35 +422,155 @@ export class FileManager {
canvas.width = width;
canvas.height = height;
-
- const ctx = canvas.getContext('2d');
+
+ const ctx = canvas.getContext("2d");
ctx?.drawImage(img, 0, 0, width, height);
-
+
// Convert to blob with higher quality for larger files
canvas.toBlob(
(blob) => {
if (!blob) {
- reject(new Error('Failed to compress image'));
+ reject(new Error("Failed to compress image"));
return;
}
- resolve(new File([blob], file.name, {
- type: 'image/jpeg',
- lastModified: Date.now(),
- }));
+ resolve(
+ new File([blob], file.name, {
+ type: "image/jpeg",
+ lastModified: Date.now(),
+ }),
+ );
},
- 'image/jpeg',
- 0.85 // Higher quality setting for larger files
+ "image/jpeg",
+ 0.85, // Higher quality setting for larger files
);
};
-
+
img.onerror = () => {
- reject(new Error('Failed to load image for compression'));
+ reject(new Error("Failed to load image for compression"));
};
};
-
+
reader.onerror = () => {
- reject(new Error('Failed to read file for compression'));
+ reject(new Error("Failed to read file for compression"));
};
});
}
-}
\ No newline at end of file
+
+ /**
+ * Get a file token for accessing protected files
+ * @returns Promise The file token
+ */
+ public async getFileToken(): Promise {
+ // Check authentication status
+ if (!this.auth.isAuthenticated()) {
+ console.warn("User is not authenticated when trying to get file token");
+
+ // Try to refresh the auth if possible
+ try {
+ const pb = this.auth.getPocketBase();
+ if (pb.authStore.isValid) {
+ console.log(
+ "Auth store is valid, but auth check failed. Trying to refresh token.",
+ );
+ await pb.collection("users").authRefresh();
+ console.log("Auth refreshed successfully");
+ } else {
+ throw new Error("User must be authenticated to get a file token");
+ }
+ } catch (refreshError) {
+ console.error("Failed to refresh authentication:", refreshError);
+ throw new Error("User must be authenticated to get a file token");
+ }
+ }
+
+ try {
+ this.auth.setUpdating(true);
+ const pb = this.auth.getPocketBase();
+
+ // Log auth status
+ console.log("Auth status before getting token:", {
+ isValid: pb.authStore.isValid,
+ token: pb.authStore.token
+ ? pb.authStore.token.substring(0, 10) + "..."
+ : "none",
+ model: pb.authStore.model ? pb.authStore.model.id : "none",
+ });
+
+ const result = await pb.files.getToken();
+ console.log("Got file token:", result.substring(0, 10) + "...");
+ return result;
+ } catch (err) {
+ console.error("Failed to get file token:", err);
+ throw err;
+ } finally {
+ this.auth.setUpdating(false);
+ }
+ }
+
+ /**
+ * Get a file URL with an optional token for protected files
+ * @param collectionName The name of the collection
+ * @param recordId The ID of the record containing the file
+ * @param filename The name of the file
+ * @param useToken Whether to include a token for protected files
+ * @returns Promise The file URL with token if requested
+ */
+ public async getFileUrlWithToken(
+ collectionName: string,
+ recordId: string,
+ filename: string,
+ useToken: boolean = false,
+ ): Promise {
+ const pb = this.auth.getPocketBase();
+
+ // Check if filename is empty
+ if (!filename) {
+ console.error(
+ `Empty filename provided for ${collectionName}/${recordId}`,
+ );
+ return "";
+ }
+
+ // Check if user is authenticated
+ if (!this.auth.isAuthenticated()) {
+ console.warn("User is not authenticated when trying to get file URL");
+ }
+
+ // Always try to use token for protected files
+ if (useToken) {
+ try {
+ console.log(
+ `Getting file token for ${collectionName}/${recordId}/${filename}`,
+ );
+ const token = await this.getFileToken();
+ console.log(`Got token: ${token.substring(0, 10)}...`);
+
+ // Make sure to pass the token as a query parameter
+ const url = pb.files.getURL(
+ { id: recordId, collectionId: collectionName },
+ filename,
+ { token },
+ );
+ console.log(`Generated URL with token: ${url.substring(0, 50)}...`);
+ return url;
+ } catch (error) {
+ console.error("Error getting file token:", error);
+ // Fall back to URL without token
+ const url = pb.files.getURL(
+ { id: recordId, collectionId: collectionName },
+ filename,
+ );
+ console.log(`Fallback URL without token: ${url.substring(0, 50)}...`);
+ return url;
+ }
+ }
+
+ // If not using token
+ const url = pb.files.getURL(
+ { id: recordId, collectionId: collectionName },
+ filename,
+ );
+ console.log(`Generated URL without token: ${url.substring(0, 50)}...`);
+ return url;
+ }
+}