From 8c0d8cae697b45a2ad4e06f7055b4c4d8a5d9e36 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 10 Jun 2025 14:52:39 +0200 Subject: [PATCH] updates --- components/ClientEmailSection.vue | 72 +++++++++++++++------ components/EOISection.vue | 56 +++++----------- server/api/eoi/upload-document.ts | 14 ++-- server/api/files/list-with-attachments.ts | 78 ++++++++++------------- server/api/files/upload.ts | 30 +++++++-- 5 files changed, 136 insertions(+), 114 deletions(-) diff --git a/components/ClientEmailSection.vue b/components/ClientEmailSection.vue index 4baf22b..f54a637 100644 --- a/components/ClientEmailSection.vue +++ b/components/ClientEmailSection.vue @@ -137,15 +137,6 @@ > EOI Link - - Interest Form - - + + + +
+ {{ tempSelectedFiles.length }} file(s) selected +
+ + Cancel + + Attach Selected Files + +
@@ -350,6 +357,7 @@ const sessionId = ref(''); const includeSignature = ref(true); const signatureConfig = ref({}); const showFileBrowser = ref(false); +const tempSelectedFiles = ref([]); const emailDraft = ref<{ subject: string; @@ -506,10 +514,11 @@ const sendEmail = async () => { const formData = new FormData(); formData.append('file', file); - // Use the client's name for the attachment folder - const clientName = props.interest['Full Name'] + // Use the current user's username/email for the attachment folder + // Get email from stored signature config or session + const userEmail = signatureConfig.value?.email || 'unknown'; + const username = userEmail.split('@')[0] .toLowerCase() - .replace(/\s+/g, '-') .replace(/[^a-z0-9-]/g, ''); const uploadResponse = await $fetch<{ @@ -522,7 +531,7 @@ const sendEmail = async () => { 'x-tag': '094ut234' }, query: { - path: `${clientName}-attachments`, + path: `${username}-attachments/`, bucket: 'client-emails' }, body: formData @@ -581,6 +590,11 @@ const sendEmail = async () => { if (response.success) { toast.success('Email sent successfully'); + // Save signature config + if (includeSignature.value) { + localStorage.setItem('emailSignature', JSON.stringify(signatureConfig.value)); + } + // Add to thread emailThreads.value.unshift({ direction: 'sent', @@ -617,7 +631,7 @@ const closeComposer = () => { }; const removeBrowserFile = (index: number) => { - selectedBrowserFiles.value.splice(index, 1); + selectedBrowserFiles.value = selectedBrowserFiles.value.filter((_, i) => i !== index); }; const onCredentialsSaved = (data: { sessionId: string }) => { @@ -650,16 +664,34 @@ const formatEmailContent = (content: string) => { }; const openFileBrowser = () => { + tempSelectedFiles.value = [...selectedBrowserFiles.value]; // Clone existing selections showFileBrowser.value = true; }; const handleFileSelected = (file: any) => { - // Check if file is already selected - const exists = selectedBrowserFiles.value.some(f => f.name === file.name); - if (!exists) { - selectedBrowserFiles.value.push(file); + // Check if file is already selected in temp list + const existsIndex = tempSelectedFiles.value.findIndex(f => f.name === file.name); + if (existsIndex > -1) { + // Remove if already selected (toggle behavior) + tempSelectedFiles.value.splice(existsIndex, 1); + toast.info(`Removed ${file.displayName || file.name} from selection`); + } else { + // Add to temp selection + tempSelectedFiles.value.push(file); + toast.success(`Selected ${file.displayName || file.name}`); } - toast.success(`Added ${file.displayName || file.name} to attachments`); +}; + +const confirmFileSelection = () => { + selectedBrowserFiles.value = [...tempSelectedFiles.value]; + showFileBrowser.value = false; + tempSelectedFiles.value = []; + toast.success(`${selectedBrowserFiles.value.length} file(s) attached`); +}; + +const cancelFileBrowser = () => { + showFileBrowser.value = false; + tempSelectedFiles.value = []; }; diff --git a/components/EOISection.vue b/components/EOISection.vue index b31303f..1ba46e2 100644 --- a/components/EOISection.vue +++ b/components/EOISection.vue @@ -181,43 +181,21 @@
Upload a signed PDF document
- - - mdi-upload - Upload File - - - mdi-folder-open - Browse Files - - - - - - - - - - - -
- mdi-folder-open -
File browser integration coming soon
-
-
-
+ + +
@@ -232,7 +210,7 @@ variant="flat" @click="handleUpload" :loading="isUploading" - :disabled="!selectedFile && uploadTab === 'upload'" + :disabled="!selectedFile" size="large" prepend-icon="mdi-upload" > @@ -261,7 +239,6 @@ const isGenerating = ref(false); const showUploadDialog = ref(false); const isUploading = ref(false); const selectedFile = ref(null); -const uploadTab = ref('upload'); // Reminder settings const remindersEnabled = ref(true); @@ -471,6 +448,5 @@ const updateReminderSettings = async (value: boolean | null) => { const closeUploadDialog = () => { showUploadDialog.value = false; selectedFile.value = null; - uploadTab.value = 'upload'; }; diff --git a/server/api/eoi/upload-document.ts b/server/api/eoi/upload-document.ts index 986ccd7..fd15a34 100644 --- a/server/api/eoi/upload-document.ts +++ b/server/api/eoi/upload-document.ts @@ -23,8 +23,8 @@ export default defineEventHandler(async (event) => { }); } - // Ensure EOIs folder exists - await createBucketIfNotExists('nda-documents'); + // Ensure bucket exists + await createBucketIfNotExists('client-portal'); // Parse multipart form data const form = formidable({ @@ -55,15 +55,17 @@ export default defineEventHandler(async (event) => { // Get content type const contentType = mime.lookup(uploadedFile.originalFilename || '') || 'application/pdf'; - // Upload to MinIO - await uploadFile(fileName, fileBuffer, contentType); + // Upload to MinIO client-portal bucket + const client = getMinioClient(); + await client.putObject('client-portal', fileName, fileBuffer, fileBuffer.length, { + 'Content-Type': contentType, + }); // Clean up temp file await fs.unlink(uploadedFile.filepath); // Get download URL for the uploaded file - const client = getMinioClient(); - const url = await client.presignedGetObject('nda-documents', fileName, 24 * 60 * 60); // 24 hour expiry + const url = await client.presignedGetObject('client-portal', fileName, 24 * 60 * 60); // 24 hour expiry // Prepare document data for database const documentData = { diff --git a/server/api/files/list-with-attachments.ts b/server/api/files/list-with-attachments.ts index 160af72..9942b14 100644 --- a/server/api/files/list-with-attachments.ts +++ b/server/api/files/list-with-attachments.ts @@ -22,53 +22,43 @@ export default defineEventHandler(async (event) => { // If requested, also include email attachments from client-emails bucket if (includeEmailAttachments && currentUserEmail) { try { - // Get the user's full name from their interests - const { getInterests } = await import('~/server/utils/nocodb'); - const interests = await getInterests(); - const userInterest = interests.list?.find((interest: any) => - interest['Email Address']?.toLowerCase() === currentUserEmail.toLowerCase() - ); + // Create the folder name from the user's email + const username = currentUserEmail.split('@')[0] + .toLowerCase() + .replace(/[^a-z0-9-]/g, ''); - if (userInterest && userInterest['Full Name']) { - // Create the folder name from the user's name - const clientName = userInterest['Full Name'] - .toLowerCase() - .replace(/\s+/g, '-') - .replace(/[^a-z0-9-]/g, ''); - - const attachmentFolder = `${clientName}-attachments`; - - // List files from the user's attachment folder - const attachmentFiles = await listFilesFromBucket('client-emails', attachmentFolder + '/', true); - - // Add these files with a special flag to identify them as email attachments - const formattedAttachmentFiles = attachmentFiles.map((file: any) => ({ + const attachmentFolder = `${username}-attachments`; + + // List files from the user's attachment folder + const attachmentFiles = await listFilesFromBucket('client-emails', attachmentFolder + '/', true); + + // Add these files with a special flag to identify them as email attachments + const formattedAttachmentFiles = attachmentFiles.map((file: any) => ({ + ...file, + isEmailAttachment: true, + displayPath: `Email Attachments/${file.name.replace(attachmentFolder + '/', '')}`, + bucket: 'client-emails' + })); + + // Create a virtual folder for email attachments + if (formattedAttachmentFiles.length > 0 && !prefix) { + allFiles.push({ + name: 'Email Attachments/', + size: 0, + lastModified: new Date(), + etag: '', + isFolder: true, + isVirtualFolder: true, + icon: 'mdi-email-multiple' + }); + } + + // If we're inside the Email Attachments folder, show the files + if (prefix === 'Email Attachments/') { + allFiles = formattedAttachmentFiles.map((file: any) => ({ ...file, - isEmailAttachment: true, - displayPath: `Email Attachments/${file.name.replace(attachmentFolder + '/', '')}`, - bucket: 'client-emails' + name: file.name.replace(attachmentFolder + '/', '') })); - - // Create a virtual folder for email attachments - if (formattedAttachmentFiles.length > 0 && !prefix) { - allFiles.push({ - name: 'Email Attachments/', - size: 0, - lastModified: new Date(), - etag: '', - isFolder: true, - isVirtualFolder: true, - icon: 'mdi-email-multiple' - }); - } - - // If we're inside the Email Attachments folder, show the files - if (prefix === 'Email Attachments/') { - allFiles = formattedAttachmentFiles.map((file: any) => ({ - ...file, - name: file.name.replace(attachmentFolder + '/', '') - })); - } } } catch (error) { console.error('Error fetching email attachments:', error); diff --git a/server/api/files/upload.ts b/server/api/files/upload.ts index 832995f..bf9bd87 100644 --- a/server/api/files/upload.ts +++ b/server/api/files/upload.ts @@ -1,13 +1,14 @@ -import { uploadFile } from '~/server/utils/minio'; +import { uploadFile, getMinioClient } from '~/server/utils/minio'; import formidable from 'formidable'; import { promises as fs } from 'fs'; import mime from 'mime-types'; export default defineEventHandler(async (event) => { try { - // Get the current path from query params + // Get the current path and bucket from query params const query = getQuery(event); const currentPath = (query.path as string) || ''; + const bucket = (query.bucket as string) || 'client-portal'; // Default bucket // Parse multipart form data const form = formidable({ @@ -38,23 +39,44 @@ export default defineEventHandler(async (event) => { // Get content type const contentType = mime.lookup(uploadedFile.originalFilename || '') || 'application/octet-stream'; - // Upload to MinIO - await uploadFile(fullPath, fileBuffer, contentType); + // Upload to MinIO - handle different buckets + if (bucket === 'client-portal') { + await uploadFile(fullPath, fileBuffer, contentType); + } else { + // For other buckets, use the MinIO client directly + const client = getMinioClient(); + await client.putObject(bucket, fullPath, fileBuffer, fileBuffer.length, { + 'Content-Type': contentType, + }); + } // Clean up temp file await fs.unlink(uploadedFile.filepath); results.push({ fileName: fullPath, + path: fullPath, originalName: uploadedFile.originalFilename, size: uploadedFile.size, contentType, + bucket: bucket }); // Log audit event await logAuditEvent(event, 'upload', fullPath, uploadedFile.size); } + // Return the first file's info for single file uploads (backward compatibility) + if (results.length === 1) { + return { + success: true, + path: results[0].path, + fileName: results[0].fileName, + files: results, + message: `File uploaded successfully`, + }; + } + return { success: true, files: results,