This commit is contained in:
Matt 2025-06-10 14:52:39 +02:00
parent bb1a237961
commit 8c0d8cae69
5 changed files with 136 additions and 114 deletions

View File

@ -137,15 +137,6 @@
> >
EOI Link EOI Link
</v-btn> </v-btn>
<v-btn
@click="insertFormLink"
size="small"
variant="tonal"
prepend-icon="mdi-form-select"
class="ml-2"
>
Interest Form
</v-btn>
<v-btn <v-btn
v-if="hasBerth" v-if="hasBerth"
@click="insertBerthInfo" @click="insertBerthInfo"
@ -313,12 +304,28 @@
<v-btn icon="mdi-close" variant="text" @click="showFileBrowser = false"></v-btn> <v-btn icon="mdi-close" variant="text" @click="showFileBrowser = false"></v-btn>
</v-card-title> </v-card-title>
<v-divider /> <v-divider />
<v-card-text class="pa-0" style="height: calc(100% - 64px);"> <v-card-text class="pa-0" style="height: calc(100% - 64px - 72px);">
<FileBrowser <FileBrowser
selection-mode selection-mode
@file-selected="handleFileSelected" @file-selected="handleFileSelected"
/> />
</v-card-text> </v-card-text>
<v-divider />
<v-card-actions>
<div class="text-caption" v-if="tempSelectedFiles.length > 0">
{{ tempSelectedFiles.length }} file(s) selected
</div>
<v-spacer />
<v-btn @click="cancelFileBrowser" variant="text">Cancel</v-btn>
<v-btn
@click="confirmFileSelection"
color="primary"
variant="flat"
:disabled="tempSelectedFiles.length === 0"
>
Attach Selected Files
</v-btn>
</v-card-actions>
</v-card> </v-card>
</v-dialog> </v-dialog>
</div> </div>
@ -350,6 +357,7 @@ const sessionId = ref<string>('');
const includeSignature = ref(true); const includeSignature = ref(true);
const signatureConfig = ref<any>({}); const signatureConfig = ref<any>({});
const showFileBrowser = ref(false); const showFileBrowser = ref(false);
const tempSelectedFiles = ref<any[]>([]);
const emailDraft = ref<{ const emailDraft = ref<{
subject: string; subject: string;
@ -506,10 +514,11 @@ const sendEmail = async () => {
const formData = new FormData(); const formData = new FormData();
formData.append('file', file); formData.append('file', file);
// Use the client's name for the attachment folder // Use the current user's username/email for the attachment folder
const clientName = props.interest['Full Name'] // Get email from stored signature config or session
const userEmail = signatureConfig.value?.email || 'unknown';
const username = userEmail.split('@')[0]
.toLowerCase() .toLowerCase()
.replace(/\s+/g, '-')
.replace(/[^a-z0-9-]/g, ''); .replace(/[^a-z0-9-]/g, '');
const uploadResponse = await $fetch<{ const uploadResponse = await $fetch<{
@ -522,7 +531,7 @@ const sendEmail = async () => {
'x-tag': '094ut234' 'x-tag': '094ut234'
}, },
query: { query: {
path: `${clientName}-attachments`, path: `${username}-attachments/`,
bucket: 'client-emails' bucket: 'client-emails'
}, },
body: formData body: formData
@ -581,6 +590,11 @@ const sendEmail = async () => {
if (response.success) { if (response.success) {
toast.success('Email sent successfully'); toast.success('Email sent successfully');
// Save signature config
if (includeSignature.value) {
localStorage.setItem('emailSignature', JSON.stringify(signatureConfig.value));
}
// Add to thread // Add to thread
emailThreads.value.unshift({ emailThreads.value.unshift({
direction: 'sent', direction: 'sent',
@ -617,7 +631,7 @@ const closeComposer = () => {
}; };
const removeBrowserFile = (index: number) => { const removeBrowserFile = (index: number) => {
selectedBrowserFiles.value.splice(index, 1); selectedBrowserFiles.value = selectedBrowserFiles.value.filter((_, i) => i !== index);
}; };
const onCredentialsSaved = (data: { sessionId: string }) => { const onCredentialsSaved = (data: { sessionId: string }) => {
@ -650,16 +664,34 @@ const formatEmailContent = (content: string) => {
}; };
const openFileBrowser = () => { const openFileBrowser = () => {
tempSelectedFiles.value = [...selectedBrowserFiles.value]; // Clone existing selections
showFileBrowser.value = true; showFileBrowser.value = true;
}; };
const handleFileSelected = (file: any) => { const handleFileSelected = (file: any) => {
// Check if file is already selected // Check if file is already selected in temp list
const exists = selectedBrowserFiles.value.some(f => f.name === file.name); const existsIndex = tempSelectedFiles.value.findIndex(f => f.name === file.name);
if (!exists) { if (existsIndex > -1) {
selectedBrowserFiles.value.push(file); // 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 = [];
}; };
</script> </script>

View File

@ -181,43 +181,21 @@
<div class="text-body-2 text-grey">Upload a signed PDF document</div> <div class="text-body-2 text-grey">Upload a signed PDF document</div>
</div> </div>
<v-tabs v-model="uploadTab" class="mb-4"> <v-file-input
<v-tab value="upload"> v-model="selectedFile"
<v-icon start>mdi-upload</v-icon> label="Drop your PDF here or click to browse"
Upload File accept=".pdf"
</v-tab> prepend-icon=""
<v-tab value="browse"> variant="outlined"
<v-icon start>mdi-folder-open</v-icon> density="comfortable"
Browse Files :rules="[v => !!v || 'Please select a file']"
</v-tab> show-size
</v-tabs> class="mt-4"
>
<v-window v-model="uploadTab"> <template v-slot:prepend-inner>
<v-window-item value="upload"> <v-icon color="primary">mdi-file-pdf-box</v-icon>
<v-file-input </template>
v-model="selectedFile" </v-file-input>
label="Drop your PDF here or click to browse"
accept=".pdf"
prepend-icon=""
variant="outlined"
density="comfortable"
:rules="[v => !!v || 'Please select a file']"
show-size
class="mt-4"
>
<template v-slot:prepend-inner>
<v-icon color="primary">mdi-file-pdf-box</v-icon>
</template>
</v-file-input>
</v-window-item>
<v-window-item value="browse">
<div class="text-center py-8 text-grey">
<v-icon size="48" class="mb-3">mdi-folder-open</v-icon>
<div>File browser integration coming soon</div>
</div>
</v-window-item>
</v-window>
</v-card-text> </v-card-text>
<v-divider /> <v-divider />
@ -232,7 +210,7 @@
variant="flat" variant="flat"
@click="handleUpload" @click="handleUpload"
:loading="isUploading" :loading="isUploading"
:disabled="!selectedFile && uploadTab === 'upload'" :disabled="!selectedFile"
size="large" size="large"
prepend-icon="mdi-upload" prepend-icon="mdi-upload"
> >
@ -261,7 +239,6 @@ const isGenerating = ref(false);
const showUploadDialog = ref(false); const showUploadDialog = ref(false);
const isUploading = ref(false); const isUploading = ref(false);
const selectedFile = ref<File | null>(null); const selectedFile = ref<File | null>(null);
const uploadTab = ref('upload');
// Reminder settings // Reminder settings
const remindersEnabled = ref(true); const remindersEnabled = ref(true);
@ -471,6 +448,5 @@ const updateReminderSettings = async (value: boolean | null) => {
const closeUploadDialog = () => { const closeUploadDialog = () => {
showUploadDialog.value = false; showUploadDialog.value = false;
selectedFile.value = null; selectedFile.value = null;
uploadTab.value = 'upload';
}; };
</script> </script>

View File

@ -23,8 +23,8 @@ export default defineEventHandler(async (event) => {
}); });
} }
// Ensure EOIs folder exists // Ensure bucket exists
await createBucketIfNotExists('nda-documents'); await createBucketIfNotExists('client-portal');
// Parse multipart form data // Parse multipart form data
const form = formidable({ const form = formidable({
@ -55,15 +55,17 @@ export default defineEventHandler(async (event) => {
// Get content type // Get content type
const contentType = mime.lookup(uploadedFile.originalFilename || '') || 'application/pdf'; const contentType = mime.lookup(uploadedFile.originalFilename || '') || 'application/pdf';
// Upload to MinIO // Upload to MinIO client-portal bucket
await uploadFile(fileName, fileBuffer, contentType); const client = getMinioClient();
await client.putObject('client-portal', fileName, fileBuffer, fileBuffer.length, {
'Content-Type': contentType,
});
// Clean up temp file // Clean up temp file
await fs.unlink(uploadedFile.filepath); await fs.unlink(uploadedFile.filepath);
// Get download URL for the uploaded file // Get download URL for the uploaded file
const client = getMinioClient(); const url = await client.presignedGetObject('client-portal', fileName, 24 * 60 * 60); // 24 hour expiry
const url = await client.presignedGetObject('nda-documents', fileName, 24 * 60 * 60); // 24 hour expiry
// Prepare document data for database // Prepare document data for database
const documentData = { const documentData = {

View File

@ -22,53 +22,43 @@ export default defineEventHandler(async (event) => {
// If requested, also include email attachments from client-emails bucket // If requested, also include email attachments from client-emails bucket
if (includeEmailAttachments && currentUserEmail) { if (includeEmailAttachments && currentUserEmail) {
try { try {
// Get the user's full name from their interests // Create the folder name from the user's email
const { getInterests } = await import('~/server/utils/nocodb'); const username = currentUserEmail.split('@')[0]
const interests = await getInterests(); .toLowerCase()
const userInterest = interests.list?.find((interest: any) => .replace(/[^a-z0-9-]/g, '');
interest['Email Address']?.toLowerCase() === currentUserEmail.toLowerCase()
);
if (userInterest && userInterest['Full Name']) { const attachmentFolder = `${username}-attachments`;
// 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);
// List files from the user's attachment folder // Add these files with a special flag to identify them as email attachments
const attachmentFiles = await listFilesFromBucket('client-emails', attachmentFolder + '/', true); const formattedAttachmentFiles = attachmentFiles.map((file: any) => ({
...file,
isEmailAttachment: true,
displayPath: `Email Attachments/${file.name.replace(attachmentFolder + '/', '')}`,
bucket: 'client-emails'
}));
// Add these files with a special flag to identify them as email attachments // Create a virtual folder for email attachments
const formattedAttachmentFiles = attachmentFiles.map((file: any) => ({ 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, ...file,
isEmailAttachment: true, name: file.name.replace(attachmentFolder + '/', '')
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,
name: file.name.replace(attachmentFolder + '/', '')
}));
}
} }
} catch (error) { } catch (error) {
console.error('Error fetching email attachments:', error); console.error('Error fetching email attachments:', error);

View File

@ -1,13 +1,14 @@
import { uploadFile } from '~/server/utils/minio'; import { uploadFile, getMinioClient } from '~/server/utils/minio';
import formidable from 'formidable'; import formidable from 'formidable';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import mime from 'mime-types'; import mime from 'mime-types';
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
try { try {
// Get the current path from query params // Get the current path and bucket from query params
const query = getQuery(event); const query = getQuery(event);
const currentPath = (query.path as string) || ''; const currentPath = (query.path as string) || '';
const bucket = (query.bucket as string) || 'client-portal'; // Default bucket
// Parse multipart form data // Parse multipart form data
const form = formidable({ const form = formidable({
@ -38,23 +39,44 @@ export default defineEventHandler(async (event) => {
// Get content type // Get content type
const contentType = mime.lookup(uploadedFile.originalFilename || '') || 'application/octet-stream'; const contentType = mime.lookup(uploadedFile.originalFilename || '') || 'application/octet-stream';
// Upload to MinIO // Upload to MinIO - handle different buckets
await uploadFile(fullPath, fileBuffer, contentType); 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 // Clean up temp file
await fs.unlink(uploadedFile.filepath); await fs.unlink(uploadedFile.filepath);
results.push({ results.push({
fileName: fullPath, fileName: fullPath,
path: fullPath,
originalName: uploadedFile.originalFilename, originalName: uploadedFile.originalFilename,
size: uploadedFile.size, size: uploadedFile.size,
contentType, contentType,
bucket: bucket
}); });
// Log audit event // Log audit event
await logAuditEvent(event, 'upload', fullPath, uploadedFile.size); 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 { return {
success: true, success: true,
files: results, files: results,