updates
This commit is contained in:
parent
bb1a237961
commit
8c0d8cae69
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue