diff --git a/components/EOISection.vue b/components/EOISection.vue index f54eebb..560b914 100644 --- a/components/EOISection.vue +++ b/components/EOISection.vue @@ -31,7 +31,7 @@
-
-
- - Signature Status - - -
- - - - - Client Signature Link - {{ interest['Full Name'] }} - - + + + + Client Signature Link + {{ interest['Full Name'] }} + + - - - CC Signature Link - Oscar Faragher - - + + + CC Signature Link + Oscar Faragher + + - - - Developer Signature Link - David Mizrahi - - - -
+ + + Developer Signature Link + David Mizrahi + + + - -
+ +
Regenerate EOI - - - Delete Generated EOI -
@@ -331,103 +274,6 @@ - - - - - mdi-delete-alert - Delete Generated EOI Document - - - -
- - mdi-alert-triangle - This will permanently delete the generated EOI document from Documenso. - - - This action will: -
    -
  • Delete the document from Documenso platform
  • -
  • Remove all signature links
  • -
  • Reset the Sales Process Level to "Specific Qualified Interest"
  • -
  • Reset the EOI Status to "Awaiting Further Details"
  • -
  • Reset the EOI Time Sent field
  • -
  • Allow a new EOI to be generated
  • -
- -
- mdi-block-helper - Cannot delete: All parties have already signed this document. -
- -
- mdi-alert - Warning: Some parties have already signed this document. -
-
- -
- - -
- To confirm deletion, slide the handle all the way to the right: -
- -
- - - - -
- Cancel - - DELETE - -
-
-
-
- - - - - Cancel - - - mdi-delete - Delete Generated EOI - - -
-
-
@@ -451,11 +297,6 @@ const isUploading = ref(false); const selectedFile = ref(null); const showDeleteConfirmDialog = ref(false); const isDeleting = ref(false); -const signatureStatus = ref(null); -const isCheckingStatus = ref(false); -const showDeleteGeneratedDialog = ref(false); -const isDeletingGenerated = ref(false); -const deleteSliderValue = ref(0); const hasEOI = computed(() => { return !!(props.interest['Signature Link Client'] || @@ -475,42 +316,7 @@ const isEOISigned = computed(() => { return props.interest['EOI Status'] === 'Signed'; }); -const canDeleteGenerated = computed(() => { - return hasEOI.value && !isEOISigned.value && (!signatureStatus.value?.allSigned); -}); - -const checkSignatureStatus = async () => { - if (!hasEOI.value || isEOISigned.value) return; - - isCheckingStatus.value = true; - try { - const response = await $fetch<{ - success: boolean; - documentStatus: string; - unsignedRecipients: any[]; - signedRecipients: any[]; - clientSigned: boolean; - allSigned: boolean; - }>('/api/eoi/check-signature-status', { - headers: { - 'x-tag': '094ut234' - }, - params: { - interestId: props.interest.Id.toString() - } - }); - - if (response.success) { - signatureStatus.value = response; - } - } catch (error: any) { - console.error('Failed to check signature status:', error); - } finally { - isCheckingStatus.value = false; - } -}; - -const generateEOI = async (retryCount = 0, regenerate = false) => { +const generateEOI = async (retryCount = 0) => { isGenerating.value = true; try { @@ -525,17 +331,14 @@ const generateEOI = async (retryCount = 0, regenerate = false) => { 'x-tag': '094ut234' }, body: { - interestId: props.interest.Id.toString(), - regenerate: regenerate + interestId: props.interest.Id.toString() } }); if (response.success) { toast.success(response.documentId === 'existing' ? 'EOI already exists - signature links retrieved' - : regenerate - ? 'EOI regenerated successfully' - : 'EOI generated successfully'); + : 'EOI generated successfully'); emit('eoi-generated', { signingLinks: response.signingLinks }); emit('update'); // Trigger parent to refresh data @@ -549,7 +352,7 @@ const generateEOI = async (retryCount = 0, regenerate = false) => { if (retryCount < 3) { console.log(`Retrying EOI generation... Attempt ${retryCount + 2}/4`); await new Promise(resolve => setTimeout(resolve, (retryCount + 1) * 1000)); - return generateEOI(retryCount + 1, regenerate); + return generateEOI(retryCount + 1); } // Show error message after all retries failed @@ -670,6 +473,7 @@ const handleUpload = () => { } }; + const closeUploadDialog = () => { showUploadDialog.value = false; selectedFile.value = null; @@ -701,94 +505,4 @@ const deleteEOI = async () => { isDeleting.value = false; } }; - -const getSignatureStatusIcon = (recipientType: 'client' | 'cc' | 'developer') => { - if (!signatureStatus.value) return null; - - const signedRecipients = signatureStatus.value.signedRecipients || []; - const unsignedRecipients = signatureStatus.value.unsignedRecipients || []; - - // Check if this recipient type has signed - let isSigned = false; - - if (recipientType === 'client') { - isSigned = signatureStatus.value.clientSigned || false; - } else { - // For CC and Developer, check by email - const emailMap = { - cc: 'sales@portnimara.com', - developer: 'dm@portnimara.com' - }; - - const email = emailMap[recipientType]; - isSigned = signedRecipients.some((r: any) => r.email === email); - } - - return isSigned ? 'mdi-check-circle' : 'mdi-clock-outline'; -}; - -const getSignatureStatusColor = (recipientType: 'client' | 'cc' | 'developer') => { - if (!signatureStatus.value) return 'grey'; - - const signedRecipients = signatureStatus.value.signedRecipients || []; - - // Check if this recipient type has signed - let isSigned = false; - - if (recipientType === 'client') { - isSigned = signatureStatus.value.clientSigned || false; - } else { - // For CC and Developer, check by email - const emailMap = { - cc: 'sales@portnimara.com', - developer: 'dm@portnimara.com' - }; - - const email = emailMap[recipientType]; - isSigned = signedRecipients.some((r: any) => r.email === email); - } - - return isSigned ? 'success' : 'warning'; -}; - -const closeDeleteGeneratedDialog = () => { - showDeleteGeneratedDialog.value = false; - deleteSliderValue.value = 0; // Reset slider -}; - -const deleteGeneratedEOI = async () => { - if (deleteSliderValue.value < 100) return; - - isDeletingGenerated.value = true; - - try { - const response = await $fetch<{ success: boolean; message: string }>('/api/eoi/delete-generated-document', { - method: 'POST', - headers: { - 'x-tag': '094ut234' - }, - body: { - interestId: props.interest.Id.toString() - } - }); - - if (response.success) { - toast.success('Generated EOI deleted successfully'); - closeDeleteGeneratedDialog(); - emit('update'); // Refresh parent data - } - } catch (error: any) { - console.error('Failed to delete generated EOI:', error); - toast.error(error.data?.statusMessage || 'Failed to delete generated EOI'); - } finally { - isDeletingGenerated.value = false; - } -}; - -// Check signature status when component mounts and when interest changes -watchEffect(() => { - if (hasEOI.value && !isEOISigned.value) { - checkSignatureStatus(); - } -}); diff --git a/components/EmailDetailsDialog.vue b/components/EmailDetailsDialog.vue index e8d433d..0a88fd3 100644 --- a/components/EmailDetailsDialog.vue +++ b/components/EmailDetailsDialog.vue @@ -139,14 +139,25 @@ const getAttachmentName = (attachment: any) => { }; const getAttachmentUrl = (attachment: any) => { - // If attachment has a path and bucket, construct the download URL - if (attachment.path && attachment.bucket) { - return `/api/files/proxy-download?path=${encodeURIComponent(attachment.path)}&bucket=${attachment.bucket}`; + // If attachment is just a string (filename), assume it's in the client-emails bucket + if (typeof attachment === 'string') { + return `/api/files/proxy-download?fileName=${encodeURIComponent(attachment)}&bucket=client-emails`; } - // If it's just a URL, return it - if (attachment.url) return attachment.url; - // Otherwise return a placeholder - return '#'; + + // If it has a path property, use that + if (attachment?.path) { + const bucket = attachment.bucket || 'client-emails'; + return `/api/files/proxy-download?fileName=${encodeURIComponent(attachment.path)}&bucket=${bucket}`; + } + + // If it has a url property, use that directly + if (attachment?.url) { + return attachment.url; + } + + // Default fallback + const name = attachment?.name || attachment?.filename || 'attachment'; + return `/api/files/proxy-download?fileName=${encodeURIComponent(name)}&bucket=client-emails`; }; diff --git a/components/InterestDetailsModal.vue b/components/InterestDetailsModal.vue index ff5297a..d41026e 100644 --- a/components/InterestDetailsModal.vue +++ b/components/InterestDetailsModal.vue @@ -196,7 +196,7 @@ - - - - - - - + + { // Update original values originalBerths.value = [...newBerths]; + + // Show success message + if (toAdd.length > 0 || toRemove.length > 0) { + toast.success("Berths updated successfully"); + } } catch (error) { console.error("Failed to update berths:", error); - toast.error("Failed to update berths. Please try again."); + // Show more specific error message on mobile + const errorMessage = mobile.value + ? "Could not update berths. Please check your connection and try again." + : "Failed to update berths. Please try again."; + toast.error(errorMessage); // Revert to original values on error selectedBerths.value = [...originalBerths.value]; } @@ -1302,6 +1273,20 @@ const onInterestUpdated = async () => { } }; +// Handle mobile save - call the save function directly without debounce +const handleMobileSave = () => { + console.log('Mobile save button clicked'); + // Cancel any pending debounced saves + if (debouncedSaveInterest) { + debouncedSaveInterest.cancel(); + } + if (autoSave) { + autoSave.cancel(); + } + // Call save directly + saveInterest(); +}; + // Load berths when component mounts onMounted(() => { loadAvailableBerths(); diff --git a/pages/dashboard/interest-list.vue b/pages/dashboard/interest-list.vue index 4234dcc..bcbcbbe 100644 --- a/pages/dashboard/interest-list.vue +++ b/pages/dashboard/interest-list.vue @@ -125,7 +125,7 @@ :headers="headers" :items="filteredInterests" :search="search" - :sort-by="[{ key: 'Created At', order: 'desc' }, { key: 'Full Name', order: 'asc' }]" + :sort-by="[{ key: 'Id', order: 'desc' }]" must-sort hover :loading="loading" diff --git a/server/api/email/fetch-thread.ts b/server/api/email/fetch-thread.ts index af97bda..d2ddcfe 100644 --- a/server/api/email/fetch-thread.ts +++ b/server/api/email/fetch-thread.ts @@ -13,6 +13,7 @@ interface EmailMessage { timestamp: string; direction: 'sent' | 'received'; threadId?: string; + attachments?: any[]; } export default defineEventHandler(async (event) => { @@ -304,6 +305,15 @@ async function fetchImapEmails( return; } + // Extract attachments + const attachments = parsed.attachments ? parsed.attachments.map((att: any) => ({ + filename: att.filename || 'attachment', + name: att.filename || 'attachment', + size: att.size || 0, + type: att.contentType || 'application/octet-stream', + cid: att.cid || undefined + })) : []; + const email: EmailMessage = { id: parsed.messageId || `${Date.now()}-${seqno}`, from: fromEmail, @@ -312,7 +322,8 @@ async function fetchImapEmails( body: parsed.text || '', html: parsed.html || undefined, timestamp: parsed.date?.toISOString() || new Date().toISOString(), - direction: fromEmail.toLowerCase().includes(userEmail.toLowerCase()) ? 'sent' : 'received' + direction: fromEmail.toLowerCase().includes(userEmail.toLowerCase()) ? 'sent' : 'received', + attachments: attachments }; if (parsed.headers.has('in-reply-to')) { diff --git a/server/api/email/generate-eoi-document.ts b/server/api/email/generate-eoi-document.ts index 5f730b9..360ee2e 100644 --- a/server/api/email/generate-eoi-document.ts +++ b/server/api/email/generate-eoi-document.ts @@ -29,6 +29,9 @@ export default defineEventHandler(async (event) => { throw createError({ statusCode: 401, statusMessage: "unauthenticated" }); } + // Set longer timeout for this endpoint to prevent 502 errors + event.node.res.setTimeout(60000); // 60 seconds + try { const body = await readBody(event); const { interestId } = body; diff --git a/server/api/email/send.ts b/server/api/email/send.ts index 6957d87..9a5ee89 100644 --- a/server/api/email/send.ts +++ b/server/api/email/send.ts @@ -181,7 +181,8 @@ export default defineEventHandler(async (event) => { html: htmlBody, timestamp: new Date().toISOString(), direction: 'sent', - interestId: interestId + interestId: interestId, + attachments: attachments // Include attachment info }; const objectName = `interest-${interestId}/${Date.now()}-sent.json`; diff --git a/server/utils/nocodb.ts b/server/utils/nocodb.ts index 3d79a59..fd7c3e6 100644 --- a/server/utils/nocodb.ts +++ b/server/utils/nocodb.ts @@ -196,6 +196,7 @@ export const createInterest = async (data: Partial) => { "Date Added", "Width", "Depth", + "Created At", "Source", "Contact Method Preferred", "Lead Category", @@ -213,6 +214,11 @@ export const createInterest = async (data: Partial) => { } } + // Set Created At to current timestamp if not provided + if (!cleanData["Created At"]) { + cleanData["Created At"] = new Date().toISOString(); + } + // Remove any computed or relation fields that shouldn't be sent delete cleanData.Id; delete cleanData.Berths;