From a49322f852790939e4e6709b29dbd815cb90a6ad Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 11 Jun 2025 13:50:51 +0200 Subject: [PATCH] updates --- components/EOISection.vue | 410 +++++++++++++++++--- server/api/email/generate-eoi-document.ts | 88 ++++- server/api/eoi/delete-generated-document.ts | 148 +++++++ 3 files changed, 571 insertions(+), 75 deletions(-) create mode 100644 server/api/eoi/delete-generated-document.ts diff --git a/components/EOISection.vue b/components/EOISection.vue index 560b914..f54eebb 100644 --- a/components/EOISection.vue +++ b/components/EOISection.vue @@ -31,7 +31,7 @@
- - - - Client Signature Link - {{ interest['Full Name'] }} - - +
+
+ + Signature Status + + +
+ + + + + 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 +
@@ -274,6 +331,103 @@ + + + + + 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 + + +
+
+
@@ -297,6 +451,11 @@ 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'] || @@ -316,7 +475,42 @@ const isEOISigned = computed(() => { return props.interest['EOI Status'] === 'Signed'; }); -const generateEOI = async (retryCount = 0) => { +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) => { isGenerating.value = true; try { @@ -331,14 +525,17 @@ const generateEOI = async (retryCount = 0) => { 'x-tag': '094ut234' }, body: { - interestId: props.interest.Id.toString() + interestId: props.interest.Id.toString(), + regenerate: regenerate } }); if (response.success) { toast.success(response.documentId === 'existing' ? 'EOI already exists - signature links retrieved' - : 'EOI generated successfully'); + : regenerate + ? 'EOI regenerated successfully' + : 'EOI generated successfully'); emit('eoi-generated', { signingLinks: response.signingLinks }); emit('update'); // Trigger parent to refresh data @@ -352,7 +549,7 @@ const generateEOI = async (retryCount = 0) => { 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); + return generateEOI(retryCount + 1, regenerate); } // Show error message after all retries failed @@ -473,7 +670,6 @@ const handleUpload = () => { } }; - const closeUploadDialog = () => { showUploadDialog.value = false; selectedFile.value = null; @@ -505,4 +701,94 @@ 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/server/api/email/generate-eoi-document.ts b/server/api/email/generate-eoi-document.ts index b401fa3..5f730b9 100644 --- a/server/api/email/generate-eoi-document.ts +++ b/server/api/email/generate-eoi-document.ts @@ -43,19 +43,83 @@ export default defineEventHandler(async (event) => { throw createError({ statusCode: 404, statusMessage: "Interest not found" }); } + // Documenso API configuration (declare early for use in regeneration) + const documensoApiKey = process.env.NUXT_DOCUMENSO_API_KEY; + const documensoBaseUrl = process.env.NUXT_DOCUMENSO_BASE_URL; + // Check if EOI already exists (has signature links) - if (interest['Signature Link Client'] && interest['Signature Link CC'] && interest['Signature Link Developer']) { - console.log('EOI already exists, returning existing links'); - return { - success: true, - documentId: 'existing', - clientSigningUrl: interest['Signature Link Client'], - signingLinks: { - 'Client': interest['Signature Link Client'], - 'CC': interest['Signature Link CC'], - 'Developer': interest['Signature Link Developer'] + const hasExistingEOI = !!(interest['Signature Link Client'] && interest['Signature Link CC'] && interest['Signature Link Developer']); + + if (hasExistingEOI) { + console.log('EOI already exists, checking if regeneration is needed'); + + // For regeneration, we need to delete the old document first + const regenerateRequested = body.regenerate === true; + + if (!regenerateRequested) { + console.log('Returning existing EOI links'); + return { + success: true, + documentId: 'existing', + clientSigningUrl: interest['Signature Link Client'], + signingLinks: { + 'Client': interest['Signature Link Client'], + 'CC': interest['Signature Link CC'], + 'Developer': interest['Signature Link Developer'] + } + }; + } else { + console.log('Regeneration requested, deleting old document first'); + + // Try to delete the old document from Documenso + try { + const externalId = `loi-${interestId}`; + + // Import the delete utility functions + const { getDocumesoDocumentByExternalId, checkDocumentSignatureStatus } = await import('~/server/utils/documeso'); + + const document = await getDocumesoDocumentByExternalId(externalId); + if (document) { + // Check if all parties have signed + const signatureStatus = await checkDocumentSignatureStatus(document.id); + + if (signatureStatus.allSigned) { + throw createError({ + statusCode: 403, + statusMessage: 'Cannot regenerate: All parties have already signed this document', + }); + } + + console.log('Deleting old document from Documenso:', document.id); + + const deleteResponse = await fetch(`${documensoBaseUrl}/api/v1/documents/${document.id}`, { + method: 'DELETE', + headers: { + 'Authorization': `Bearer ${documensoApiKey}`, + 'Content-Type': 'application/json' + } + }); + + if (!deleteResponse.ok) { + console.warn('Failed to delete old document from Documenso, continuing with new generation'); + } else { + console.log('Successfully deleted old document from Documenso'); + } + } + } catch (error: any) { + console.warn('Error during old document cleanup:', error.message); + // Continue with new document generation even if cleanup fails } - }; + + // Reset signature links so new ones will be generated + await updateInterest(interestId, { + 'Signature Link Client': undefined, + 'Signature Link CC': undefined, + 'Signature Link Developer': undefined + }); + + console.log('Old document cleanup completed, proceeding with new generation'); + } } // Validate required fields @@ -105,8 +169,6 @@ export default defineEventHandler(async (event) => { const berthNumbers = berths.map(b => b['Mooring Number']).join(', '); // Documenso API configuration - const documensoApiKey = process.env.NUXT_DOCUMENSO_API_KEY; - const documensoBaseUrl = process.env.NUXT_DOCUMENSO_BASE_URL; const templateId = '9'; if (!documensoApiKey || !documensoBaseUrl) { diff --git a/server/api/eoi/delete-generated-document.ts b/server/api/eoi/delete-generated-document.ts new file mode 100644 index 0000000..c98943e --- /dev/null +++ b/server/api/eoi/delete-generated-document.ts @@ -0,0 +1,148 @@ +import { getInterestById, updateInterest } from '~/server/utils/nocodb'; +import { getDocumesoDocumentByExternalId, checkDocumentSignatureStatus } from '~/server/utils/documeso'; + +export default defineEventHandler(async (event) => { + const xTagHeader = getRequestHeader(event, "x-tag"); + + console.log('[EOI Delete Generated] Request received with x-tag:', xTagHeader); + + if (!xTagHeader || (xTagHeader !== "094ut234" && xTagHeader !== "pjnvü1230")) { + console.error('[EOI Delete Generated] Authentication failed - invalid x-tag'); + throw createError({ statusCode: 401, statusMessage: "unauthenticated" }); + } + + try { + const body = await readBody(event); + const { interestId } = body; + + console.log('[EOI Delete Generated] Interest ID:', interestId); + + if (!interestId) { + console.error('[EOI Delete Generated] No interest ID provided'); + throw createError({ + statusCode: 400, + statusMessage: 'Interest ID is required', + }); + } + + // Get current interest data + const interest = await getInterestById(interestId); + if (!interest) { + throw createError({ + statusCode: 404, + statusMessage: 'Interest not found', + }); + } + + // Check if there are signature links (indicating a generated document) + const hasSignatureLinks = !!(interest['Signature Link Client'] || + interest['Signature Link CC'] || + interest['Signature Link Developer']); + + if (!hasSignatureLinks) { + throw createError({ + statusCode: 400, + statusMessage: 'No generated EOI document found for this interest', + }); + } + + // Check signature status to prevent deletion of fully signed documents + const externalId = `loi-${interestId}`; + let documentId: number | null = null; + + try { + const document = await getDocumesoDocumentByExternalId(externalId); + if (document) { + documentId = document.id; + + // Check if all parties have signed + const signatureStatus = await checkDocumentSignatureStatus(documentId); + + if (signatureStatus.allSigned) { + throw createError({ + statusCode: 403, + statusMessage: 'Cannot delete: All parties have already signed this document', + }); + } + + console.log('[EOI Delete Generated] Document found in Documenso:', documentId); + console.log('[EOI Delete Generated] Signature status:', { + allSigned: signatureStatus.allSigned, + clientSigned: signatureStatus.clientSigned, + signedCount: signatureStatus.signedRecipients.length, + unsignedCount: signatureStatus.unsignedRecipients.length + }); + } + } catch (error: any) { + console.warn('[EOI Delete Generated] Could not find or check Documenso document:', error.message); + // Continue with deletion even if Documenso document not found + } + + // Delete document from Documenso if it exists + if (documentId) { + try { + const documensoApiKey = process.env.NUXT_DOCUMENSO_API_KEY; + const documensoBaseUrl = process.env.NUXT_DOCUMENSO_BASE_URL; + + if (!documensoApiKey || !documensoBaseUrl) { + console.error('[EOI Delete Generated] Documenso configuration missing'); + throw createError({ + statusCode: 500, + statusMessage: 'Documenso configuration missing', + }); + } + + console.log('[EOI Delete Generated] Deleting document from Documenso:', documentId); + + const deleteResponse = await fetch(`${documensoBaseUrl}/api/v1/documents/${documentId}`, { + method: 'DELETE', + headers: { + 'Authorization': `Bearer ${documensoApiKey}`, + 'Content-Type': 'application/json' + } + }); + + if (!deleteResponse.ok) { + const errorText = await deleteResponse.text(); + console.error('[EOI Delete Generated] Failed to delete from Documenso:', errorText); + throw new Error(`Failed to delete document from Documenso: ${deleteResponse.statusText}`); + } + + console.log('[EOI Delete Generated] Successfully deleted document from Documenso'); + } catch (error: any) { + console.error('[EOI Delete Generated] Error deleting from Documenso:', error); + // Don't throw here - continue with database cleanup even if Documenso deletion fails + } + } + + // Reset interest fields + const updateData = { + 'EOI Status': 'Awaiting Further Details' as const, + 'Sales Process Level': 'Specific Qualified Interest' as const, + 'EOI Time Sent': undefined, + 'Signature Link Client': undefined, + 'Signature Link CC': undefined, + 'Signature Link Developer': undefined, + 'Documeso ID': undefined, + 'reminder_enabled': false + }; + + console.log('[EOI Delete Generated] Resetting interest fields'); + + // Update the interest + await updateInterest(interestId, updateData); + + console.log('[EOI Delete Generated] Delete completed successfully'); + return { + success: true, + message: 'Generated EOI document deleted successfully', + }; + } catch (error: any) { + console.error('[EOI Delete Generated] Failed to delete generated EOI document:', error); + console.error('[EOI Delete Generated] Error stack:', error.stack); + throw createError({ + statusCode: error.statusCode || 500, + statusMessage: error.statusMessage || error.message || 'Failed to delete generated EOI document', + }); + } +});