updates
This commit is contained in:
parent
8c0d8cae69
commit
4b6d3fd991
|
|
@ -14,8 +14,8 @@
|
||||||
|
|
||||||
<div v-else>
|
<div v-else>
|
||||||
|
|
||||||
<!-- Compose New Email -->
|
<!-- Email Controls -->
|
||||||
<div class="mb-4 d-flex align-center gap-2">
|
<div class="mb-4 d-flex align-center gap-2 flex-wrap">
|
||||||
<v-btn
|
<v-btn
|
||||||
@click="showComposer = true"
|
@click="showComposer = true"
|
||||||
color="primary"
|
color="primary"
|
||||||
|
|
@ -33,6 +33,24 @@
|
||||||
>
|
>
|
||||||
<v-tooltip activator="parent">Refresh Emails</v-tooltip>
|
<v-tooltip activator="parent">Refresh Emails</v-tooltip>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
@click="reconnectEmail"
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
prepend-icon="mdi-connection"
|
||||||
|
>
|
||||||
|
Reconnect
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
@click="disconnectEmail"
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
color="error"
|
||||||
|
prepend-icon="mdi-logout"
|
||||||
|
>
|
||||||
|
Disconnect
|
||||||
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Email Thread List -->
|
<!-- Email Thread List -->
|
||||||
|
|
@ -415,8 +433,14 @@ watch(() => props.interest.Id, () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const loadEmailThread = async () => {
|
const loadEmailThread = async () => {
|
||||||
|
if (!sessionId.value) {
|
||||||
|
console.log('[ClientEmailSection] No sessionId available, skipping email thread load');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
isRefreshing.value = true;
|
isRefreshing.value = true;
|
||||||
try {
|
try {
|
||||||
|
console.log('[ClientEmailSection] Loading email thread for:', props.interest['Email Address']);
|
||||||
const response = await $fetch<{
|
const response = await $fetch<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
emails: any[];
|
emails: any[];
|
||||||
|
|
@ -426,15 +450,18 @@ const loadEmailThread = async () => {
|
||||||
'x-tag': '094ut234'
|
'x-tag': '094ut234'
|
||||||
},
|
},
|
||||||
body: {
|
body: {
|
||||||
email: props.interest['Email Address']
|
clientEmail: props.interest['Email Address'],
|
||||||
|
interestId: props.interest.Id.toString(),
|
||||||
|
sessionId: sessionId.value
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
|
console.log('[ClientEmailSection] Successfully loaded', response.emails?.length || 0, 'emails');
|
||||||
emailThreads.value = response.emails || [];
|
emailThreads.value = response.emails || [];
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load email thread:', error);
|
console.error('[ClientEmailSection] Failed to load email thread:', error);
|
||||||
} finally {
|
} finally {
|
||||||
isRefreshing.value = false;
|
isRefreshing.value = false;
|
||||||
}
|
}
|
||||||
|
|
@ -641,6 +668,24 @@ const onCredentialsSaved = (data: { sessionId: string }) => {
|
||||||
toast.success('Email credentials saved successfully');
|
toast.success('Email credentials saved successfully');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const disconnectEmail = () => {
|
||||||
|
// Clear stored credentials
|
||||||
|
localStorage.removeItem('emailSessionId');
|
||||||
|
localStorage.removeItem('emailSignature');
|
||||||
|
sessionId.value = '';
|
||||||
|
hasEmailCredentials.value = false;
|
||||||
|
emailThreads.value = [];
|
||||||
|
toast.success('Email disconnected');
|
||||||
|
};
|
||||||
|
|
||||||
|
const reconnectEmail = () => {
|
||||||
|
// Clear current session to force re-authentication
|
||||||
|
localStorage.removeItem('emailSessionId');
|
||||||
|
sessionId.value = '';
|
||||||
|
hasEmailCredentials.value = false;
|
||||||
|
toast.info('Please enter your email credentials');
|
||||||
|
};
|
||||||
|
|
||||||
const formatDate = (dateString: string) => {
|
const formatDate = (dateString: string) => {
|
||||||
if (!dateString) return '';
|
if (!dateString) return '';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,10 @@ import { updateInterest } from '~/server/utils/nocodb';
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const xTagHeader = getRequestHeader(event, "x-tag");
|
const xTagHeader = getRequestHeader(event, "x-tag");
|
||||||
|
|
||||||
|
console.log('[Email Send] Request received with x-tag:', xTagHeader);
|
||||||
|
|
||||||
if (!xTagHeader || (xTagHeader !== "094ut234" && xTagHeader !== "pjnvü1230")) {
|
if (!xTagHeader || (xTagHeader !== "094ut234" && xTagHeader !== "pjnvü1230")) {
|
||||||
|
console.error('[Email Send] Authentication failed - invalid x-tag');
|
||||||
throw createError({ statusCode: 401, statusMessage: "unauthenticated" });
|
throw createError({ statusCode: 401, statusMessage: "unauthenticated" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -23,7 +26,22 @@ export default defineEventHandler(async (event) => {
|
||||||
attachments = []
|
attachments = []
|
||||||
} = body;
|
} = body;
|
||||||
|
|
||||||
|
console.log('[Email Send] Request body:', {
|
||||||
|
to,
|
||||||
|
subject,
|
||||||
|
hasBody: !!emailBody,
|
||||||
|
interestId,
|
||||||
|
hasSessionId: !!sessionId,
|
||||||
|
attachmentCount: attachments.length
|
||||||
|
});
|
||||||
|
|
||||||
if (!to || !subject || !emailBody || !sessionId) {
|
if (!to || !subject || !emailBody || !sessionId) {
|
||||||
|
console.error('[Email Send] Missing required fields:', {
|
||||||
|
hasTo: !!to,
|
||||||
|
hasSubject: !!subject,
|
||||||
|
hasBody: !!emailBody,
|
||||||
|
hasSessionId: !!sessionId
|
||||||
|
});
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
statusMessage: "To, subject, body, and sessionId are required"
|
statusMessage: "To, subject, body, and sessionId are required"
|
||||||
|
|
@ -31,8 +49,10 @@ export default defineEventHandler(async (event) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get encrypted credentials from session
|
// Get encrypted credentials from session
|
||||||
|
console.log('[Email Send] Getting credentials for sessionId:', sessionId);
|
||||||
const encryptedCredentials = getCredentialsFromSession(sessionId);
|
const encryptedCredentials = getCredentialsFromSession(sessionId);
|
||||||
if (!encryptedCredentials) {
|
if (!encryptedCredentials) {
|
||||||
|
console.error('[Email Send] No credentials found for sessionId:', sessionId);
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 401,
|
statusCode: 401,
|
||||||
statusMessage: "Email credentials not found. Please reconnect."
|
statusMessage: "Email credentials not found. Please reconnect."
|
||||||
|
|
@ -40,7 +60,22 @@ export default defineEventHandler(async (event) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decrypt credentials
|
// Decrypt credentials
|
||||||
const { email, password } = decryptCredentials(encryptedCredentials);
|
console.log('[Email Send] Decrypting credentials');
|
||||||
|
let email: string;
|
||||||
|
let password: string;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const decrypted = decryptCredentials(encryptedCredentials);
|
||||||
|
email = decrypted.email;
|
||||||
|
password = decrypted.password;
|
||||||
|
console.log('[Email Send] Successfully decrypted credentials for:', email);
|
||||||
|
} catch (decryptError) {
|
||||||
|
console.error('[Email Send] Failed to decrypt credentials:', decryptError);
|
||||||
|
throw createError({
|
||||||
|
statusCode: 401,
|
||||||
|
statusMessage: "Failed to decrypt email credentials. Please reconnect."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Get user info for signature
|
// Get user info for signature
|
||||||
const defaultName = email.split('@')[0].replace('.', ' ').replace(/\b\w/g, l => l.toUpperCase());
|
const defaultName = email.split('@')[0].replace('.', ' ').replace(/\b\w/g, l => l.toUpperCase());
|
||||||
|
|
@ -84,6 +119,7 @@ export default defineEventHandler(async (event) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Prepare email attachments
|
// Prepare email attachments
|
||||||
|
console.log('[Email Send] Processing', attachments.length, 'attachments');
|
||||||
const emailAttachments = [];
|
const emailAttachments = [];
|
||||||
if (attachments && attachments.length > 0) {
|
if (attachments && attachments.length > 0) {
|
||||||
const client = getMinioClient();
|
const client = getMinioClient();
|
||||||
|
|
@ -92,6 +128,7 @@ export default defineEventHandler(async (event) => {
|
||||||
try {
|
try {
|
||||||
// Determine which bucket to use
|
// Determine which bucket to use
|
||||||
const bucket = attachment.bucket || 'client-portal'; // Default to client-portal
|
const bucket = attachment.bucket || 'client-portal'; // Default to client-portal
|
||||||
|
console.log('[Email Send] Processing attachment:', attachment.name, 'from bucket:', bucket, 'path:', attachment.path);
|
||||||
|
|
||||||
// Download file from MinIO
|
// Download file from MinIO
|
||||||
const stream = await client.getObject(bucket, attachment.path);
|
const stream = await client.getObject(bucket, attachment.path);
|
||||||
|
|
@ -104,13 +141,14 @@ export default defineEventHandler(async (event) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const content = Buffer.concat(chunks);
|
const content = Buffer.concat(chunks);
|
||||||
|
console.log('[Email Send] Successfully downloaded attachment:', attachment.name, 'size:', content.length);
|
||||||
|
|
||||||
emailAttachments.push({
|
emailAttachments.push({
|
||||||
filename: attachment.name,
|
filename: attachment.name,
|
||||||
content: content
|
content: content
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to attach file ${attachment.name} from bucket ${attachment.bucket}:`, error);
|
console.error(`[Email Send] Failed to attach file ${attachment.name} from bucket ${attachment.bucket}:`, error);
|
||||||
// Continue with other attachments
|
// Continue with other attachments
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -118,6 +156,8 @@ export default defineEventHandler(async (event) => {
|
||||||
|
|
||||||
// Send email
|
// Send email
|
||||||
const fromName = sig.name || defaultName;
|
const fromName = sig.name || defaultName;
|
||||||
|
console.log('[Email Send] Sending email from:', email, 'to:', to);
|
||||||
|
|
||||||
const info = await transporter.sendMail({
|
const info = await transporter.sendMail({
|
||||||
from: `"${fromName}" <${email}>`,
|
from: `"${fromName}" <${email}>`,
|
||||||
to: to,
|
to: to,
|
||||||
|
|
@ -127,6 +167,8 @@ export default defineEventHandler(async (event) => {
|
||||||
attachments: emailAttachments
|
attachments: emailAttachments
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('[Email Send] Email sent successfully, messageId:', info.messageId);
|
||||||
|
|
||||||
// Store email in MinIO for thread history and update EOI Time Sent
|
// Store email in MinIO for thread history and update EOI Time Sent
|
||||||
if (interestId) {
|
if (interestId) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -175,7 +217,8 @@ export default defineEventHandler(async (event) => {
|
||||||
messageId: info.messageId
|
messageId: info.messageId
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to send email:', error);
|
console.error('[Email Send] Failed to send email:', error);
|
||||||
|
console.error('[Email Send] Error stack:', error instanceof Error ? error.stack : 'No stack trace');
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 500,
|
statusCode: 500,
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,10 @@ import mime from 'mime-types';
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const xTagHeader = getRequestHeader(event, "x-tag");
|
const xTagHeader = getRequestHeader(event, "x-tag");
|
||||||
|
|
||||||
|
console.log('[EOI Upload] Request received with x-tag:', xTagHeader);
|
||||||
|
|
||||||
if (!xTagHeader || (xTagHeader !== "094ut234" && xTagHeader !== "pjnvü1230")) {
|
if (!xTagHeader || (xTagHeader !== "094ut234" && xTagHeader !== "pjnvü1230")) {
|
||||||
|
console.error('[EOI Upload] Authentication failed - invalid x-tag');
|
||||||
throw createError({ statusCode: 401, statusMessage: "unauthenticated" });
|
throw createError({ statusCode: 401, statusMessage: "unauthenticated" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -16,7 +19,10 @@ export default defineEventHandler(async (event) => {
|
||||||
const query = getQuery(event);
|
const query = getQuery(event);
|
||||||
const interestId = query.interestId as string;
|
const interestId = query.interestId as string;
|
||||||
|
|
||||||
|
console.log('[EOI Upload] Interest ID:', interestId);
|
||||||
|
|
||||||
if (!interestId) {
|
if (!interestId) {
|
||||||
|
console.error('[EOI Upload] No interest ID provided');
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
statusMessage: 'Interest ID is required',
|
statusMessage: 'Interest ID is required',
|
||||||
|
|
@ -24,6 +30,7 @@ export default defineEventHandler(async (event) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure bucket exists
|
// Ensure bucket exists
|
||||||
|
console.log('[EOI Upload] Ensuring client-portal bucket exists');
|
||||||
await createBucketIfNotExists('client-portal');
|
await createBucketIfNotExists('client-portal');
|
||||||
|
|
||||||
// Parse multipart form data
|
// Parse multipart form data
|
||||||
|
|
@ -32,39 +39,61 @@ export default defineEventHandler(async (event) => {
|
||||||
keepExtensions: true,
|
keepExtensions: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('[EOI Upload] Parsing multipart form data');
|
||||||
const [fields, files] = await form.parse(event.node.req);
|
const [fields, files] = await form.parse(event.node.req);
|
||||||
|
|
||||||
// Handle the uploaded file
|
// Handle the uploaded file
|
||||||
const uploadedFile = Array.isArray(files.file) ? files.file[0] : files.file;
|
const uploadedFile = Array.isArray(files.file) ? files.file[0] : files.file;
|
||||||
|
|
||||||
if (!uploadedFile) {
|
if (!uploadedFile) {
|
||||||
|
console.error('[EOI Upload] No file uploaded in request');
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
statusMessage: 'No file uploaded',
|
statusMessage: 'No file uploaded',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('[EOI Upload] File received:', {
|
||||||
|
originalFilename: uploadedFile.originalFilename,
|
||||||
|
size: uploadedFile.size,
|
||||||
|
mimetype: uploadedFile.mimetype
|
||||||
|
});
|
||||||
|
|
||||||
// Read file buffer
|
// Read file buffer
|
||||||
|
console.log('[EOI Upload] Reading file from disk');
|
||||||
const fileBuffer = await fs.readFile(uploadedFile.filepath);
|
const fileBuffer = await fs.readFile(uploadedFile.filepath);
|
||||||
|
|
||||||
// Generate filename with timestamp
|
// Generate filename with timestamp - ensure it goes to EOIs folder
|
||||||
const timestamp = Date.now();
|
const timestamp = Date.now();
|
||||||
const sanitizedName = uploadedFile.originalFilename?.replace(/[^a-zA-Z0-9.-]/g, '_') || 'eoi-document.pdf';
|
const sanitizedName = uploadedFile.originalFilename?.replace(/[^a-zA-Z0-9.-]/g, '_') || 'eoi-document.pdf';
|
||||||
const fileName = `EOIs/${interestId}-${timestamp}-${sanitizedName}`;
|
const fileName = `EOIs/${interestId}-${timestamp}-${sanitizedName}`;
|
||||||
|
|
||||||
|
console.log('[EOI Upload] Generated filename:', fileName);
|
||||||
|
|
||||||
// Get content type
|
// Get content type
|
||||||
const contentType = mime.lookup(uploadedFile.originalFilename || '') || 'application/pdf';
|
const contentType = mime.lookup(uploadedFile.originalFilename || '') || 'application/pdf';
|
||||||
|
console.log('[EOI Upload] Content type:', contentType);
|
||||||
|
|
||||||
// Upload to MinIO client-portal bucket
|
// Upload to MinIO client-portal bucket
|
||||||
|
console.log('[EOI Upload] Uploading to MinIO client-portal bucket');
|
||||||
const client = getMinioClient();
|
const client = getMinioClient();
|
||||||
await client.putObject('client-portal', fileName, fileBuffer, fileBuffer.length, {
|
|
||||||
'Content-Type': contentType,
|
try {
|
||||||
});
|
await client.putObject('client-portal', fileName, fileBuffer, fileBuffer.length, {
|
||||||
|
'Content-Type': contentType,
|
||||||
|
});
|
||||||
|
console.log('[EOI Upload] Successfully uploaded to MinIO');
|
||||||
|
} catch (minioError: any) {
|
||||||
|
console.error('[EOI Upload] MinIO upload failed:', minioError);
|
||||||
|
throw new Error(`Failed to upload to MinIO: ${minioError.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Clean up temp file
|
// Clean up temp file
|
||||||
|
console.log('[EOI Upload] Cleaning 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
|
||||||
|
console.log('[EOI Upload] Generating presigned URL');
|
||||||
const url = await client.presignedGetObject('client-portal', 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
|
// Prepare document data for database
|
||||||
|
|
@ -77,6 +106,7 @@ export default defineEventHandler(async (event) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update interest with EOI document information
|
// Update interest with EOI document information
|
||||||
|
console.log('[EOI Upload] Updating interest with EOI document info');
|
||||||
await updateInterestEOIDocument(interestId, documentData);
|
await updateInterestEOIDocument(interestId, documentData);
|
||||||
|
|
||||||
// Also update the status fields
|
// Also update the status fields
|
||||||
|
|
@ -84,6 +114,7 @@ export default defineEventHandler(async (event) => {
|
||||||
'EOI Status': 'Waiting for Signatures',
|
'EOI Status': 'Waiting for Signatures',
|
||||||
'EOI Time Sent': new Date().toISOString()
|
'EOI Time Sent': new Date().toISOString()
|
||||||
};
|
};
|
||||||
|
console.log('[EOI Upload] Updating interest status fields');
|
||||||
|
|
||||||
// Update Sales Process Level if it's below "LOI and NDA Sent"
|
// Update Sales Process Level if it's below "LOI and NDA Sent"
|
||||||
const currentLevel = await getCurrentSalesLevel(interestId);
|
const currentLevel = await getCurrentSalesLevel(interestId);
|
||||||
|
|
@ -103,13 +134,15 @@ export default defineEventHandler(async (event) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('[EOI Upload] Upload completed successfully');
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
document: documentData,
|
document: documentData,
|
||||||
message: 'EOI document uploaded successfully',
|
message: 'EOI document uploaded successfully',
|
||||||
};
|
};
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Failed to upload EOI document:', error);
|
console.error('[EOI Upload] Failed to upload EOI document:', error);
|
||||||
|
console.error('[EOI Upload] Error stack:', error.stack);
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 500,
|
statusCode: 500,
|
||||||
statusMessage: error.message || 'Failed to upload EOI document',
|
statusMessage: error.message || 'Failed to upload EOI document',
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue