diff --git a/components/EOISection.vue b/components/EOISection.vue index 560b914..69e366f 100644 --- a/components/EOISection.vue +++ b/components/EOISection.vue @@ -28,8 +28,8 @@ - -
+ +
+ + +
+ + Uploaded EOI documents found. Remove uploaded documents to generate a new EOI. + + + Upload Additional EOI + +
- -
+ +
-
+
- - - - - Client Signature Link - {{ interest['Full Name'] }} - - + +
+
+ Signature Status + +
+ + + + + + Client Signature + + {{ getSignatureStatusText('client') }} + + + {{ interest['Full Name'] }} + + - - - CC Signature Link - Oscar Faragher - - + + + + CC Signature + + {{ getSignatureStatusText('cc') }} + + + Oscar Faragher + + - - - Developer Signature Link - David Mizrahi - - - + + + + Developer Signature + + {{ getSignatureStatusText('developer') }} + + + David Mizrahi + + + +
- -
+ +
Regenerate EOI + + + Delete Generated EOI +
@@ -162,7 +247,7 @@ size="small" prepend-icon="mdi-delete" > - Delete EOI + Delete Uploaded EOI
@@ -231,7 +316,7 @@ - + - Are you sure you want to delete the EOI document? This will: + Are you sure you want to delete the uploaded EOI document? This will:
  • Remove the uploaded EOI document
  • Reset the Sales Process Level to "Specific Qualified Interest"
  • @@ -265,7 +350,7 @@ Delete EOI @@ -273,6 +358,27 @@ + + +
@@ -296,12 +402,17 @@ const showUploadDialog = ref(false); const isUploading = ref(false); const selectedFile = ref(null); const showDeleteConfirmDialog = ref(false); +const showDeleteGeneratedConfirmDialog = ref(false); const isDeleting = ref(false); +const isDeletingGenerated = ref(false); +const signatureStatus = ref(null); +const isCheckingSignatures = ref(false); -const hasEOI = computed(() => { +const hasGeneratedEOI = computed(() => { return !!(props.interest['Signature Link Client'] || props.interest['Signature Link CC'] || - props.interest['Signature Link Developer']); + props.interest['Signature Link Developer'] || + props.interest['documensoID']); }); const eoiDocuments = computed(() => { @@ -342,6 +453,11 @@ const generateEOI = async (retryCount = 0) => { emit('eoi-generated', { signingLinks: response.signingLinks }); emit('update'); // Trigger parent to refresh data + + // Check signature status after generation + if (response.documentId !== 'existing') { + setTimeout(() => checkSignatureStatus(), 1000); + } } else { throw new Error('EOI generation failed'); } @@ -362,6 +478,70 @@ const generateEOI = async (retryCount = 0) => { } }; +const checkSignatureStatus = async () => { + if (!props.interest['documensoID']) return; + + isCheckingSignatures.value = true; + + try { + const response = await $fetch('/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 { + isCheckingSignatures.value = false; + } +}; + +const getSignatureStatusColor = (role: string) => { + if (!signatureStatus.value) return 'grey'; + + const isSigned = signatureStatus.value.signedRecipients?.some((r: any) => { + if (role === 'client') return r.email === props.interest['Email Address']; + if (role === 'cc') return r.email === 'sales@portnimara.com'; + if (role === 'developer') return r.email === 'dm@portnimara.com'; + return false; + }); + + return isSigned ? 'success' : 'warning'; +}; + +const getSignatureStatusIcon = (role: string) => { + if (!signatureStatus.value) return 'mdi-clock-outline'; + + const isSigned = signatureStatus.value.signedRecipients?.some((r: any) => { + if (role === 'client') return r.email === props.interest['Email Address']; + if (role === 'cc') return r.email === 'sales@portnimara.com'; + if (role === 'developer') return r.email === 'dm@portnimara.com'; + return false; + }); + + return isSigned ? 'mdi-check' : 'mdi-clock-outline'; +}; + +const getSignatureStatusText = (role: string) => { + if (!signatureStatus.value) return 'Checking...'; + + const isSigned = signatureStatus.value.signedRecipients?.some((r: any) => { + if (role === 'client') return r.email === props.interest['Email Address']; + if (role === 'cc') return r.email === 'sales@portnimara.com'; + if (role === 'developer') return r.email === 'dm@portnimara.com'; + return false; + }); + + return isSigned ? 'Signed' : 'Pending'; +}; + const copyLink = async (link: string | undefined) => { if (!link) return; @@ -473,13 +653,12 @@ const handleUpload = () => { } }; - const closeUploadDialog = () => { showUploadDialog.value = false; selectedFile.value = null; }; -const deleteEOI = async () => { +const deleteUploadedEOI = async () => { isDeleting.value = true; try { @@ -494,15 +673,50 @@ const deleteEOI = async () => { }); if (response.success) { - toast.success('EOI deleted successfully'); + toast.success('Uploaded EOI deleted successfully'); showDeleteConfirmDialog.value = false; emit('update'); // Refresh parent data } } catch (error: any) { - console.error('Failed to delete EOI:', error); - toast.error(error.data?.statusMessage || 'Failed to delete EOI'); + console.error('Failed to delete uploaded EOI:', error); + toast.error(error.data?.statusMessage || 'Failed to delete uploaded EOI'); } finally { isDeleting.value = false; } }; + +const deleteGeneratedEOI = async () => { + 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'); + showDeleteGeneratedConfirmDialog.value = false; + signatureStatus.value = null; // Reset signature status + 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; + } +}; + +// Auto-check signature status when component mounts if there's a generated EOI +onMounted(() => { + if (hasGeneratedEOI.value && !isEOISigned.value) { + checkSignatureStatus(); + } +}); diff --git a/components/SliderConfirmation.vue b/components/SliderConfirmation.vue new file mode 100644 index 0000000..c3865ea --- /dev/null +++ b/components/SliderConfirmation.vue @@ -0,0 +1,251 @@ + + + + + diff --git a/server/api/email/generate-eoi-document.ts b/server/api/email/generate-eoi-document.ts index b401fa3..dd67899 100644 --- a/server/api/email/generate-eoi-document.ts +++ b/server/api/email/generate-eoi-document.ts @@ -43,6 +43,27 @@ export default defineEventHandler(async (event) => { throw createError({ statusCode: 404, statusMessage: "Interest not found" }); } + // Documenso API configuration - moved to top for use throughout + const documensoApiKey = process.env.NUXT_DOCUMENSO_API_KEY; + const documensoBaseUrl = process.env.NUXT_DOCUMENSO_BASE_URL; + const templateId = '9'; + + if (!documensoApiKey || !documensoBaseUrl) { + throw createError({ + statusCode: 500, + statusMessage: "Documenso configuration missing. Please check NUXT_DOCUMENSO_API_KEY and NUXT_DOCUMENSO_BASE_URL environment variables." + }); + } + + // Check if uploaded EOI documents exist - prevent generation if they do + const eoiDocuments = interest['EOI Document'] || []; + if (eoiDocuments.length > 0) { + throw createError({ + statusCode: 400, + statusMessage: "Cannot generate EOI - uploaded documents already exist. Please remove uploaded documents first." + }); + } + // 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'); @@ -58,6 +79,29 @@ export default defineEventHandler(async (event) => { }; } + // If there's an existing generated document, delete it from Documenso first + if (interest['documensoID']) { + console.log('Existing generated document found, deleting from Documenso first'); + try { + const deleteResponse = await fetch(`${documensoBaseUrl}/api/v1/documents/${interest['documensoID']}`, { + method: 'DELETE', + headers: { + 'Authorization': `Bearer ${documensoApiKey}`, + 'Content-Type': 'application/json' + } + }); + + if (deleteResponse.ok) { + console.log('Successfully deleted old document from Documenso'); + } else { + console.warn('Failed to delete old document from Documenso, continuing with new generation'); + } + } catch (error) { + console.warn('Error deleting old document from Documenso:', error); + // Continue with generation even if deletion fails + } + } + // Validate required fields const requiredFields = [ { field: 'Full Name', value: interest['Full Name'] }, @@ -104,18 +148,6 @@ export default defineEventHandler(async (event) => { // Concatenate berth numbers 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) { - throw createError({ - statusCode: 500, - statusMessage: "Documenso configuration missing. Please check NUXT_DOCUMENSO_API_KEY and NUXT_DOCUMENSO_BASE_URL environment variables." - }); - } - // 1. Get template (optional - just to verify it exists) try { const templateResponse = await fetch(`${documensoBaseUrl}/api/v1/templates/${templateId}`, { @@ -272,7 +304,8 @@ export default defineEventHandler(async (event) => { 'EOI Status': 'Waiting for Signatures', 'Sales Process Level': 'LOI and NDA Sent', // Don't set EOI Time Sent here - only when email is sent or link is copied - 'Extra Comments': updatedComments + 'Extra Comments': updatedComments, + 'documensoID': documentResponse.documentId.toString() }; // Add signing links to update data with new column names diff --git a/server/api/eoi/delete-document.ts b/server/api/eoi/delete-document.ts index a778073..564ccaa 100644 --- a/server/api/eoi/delete-document.ts +++ b/server/api/eoi/delete-document.ts @@ -64,6 +64,7 @@ export default defineEventHandler(async (event) => { 'Signature Link CC': null, 'Signature Link Developer': null, 'Documeso ID': null, + 'documensoID': null, 'reminder_enabled': false }; diff --git a/server/api/eoi/delete-generated-document.ts b/server/api/eoi/delete-generated-document.ts new file mode 100644 index 0000000..e51ebf7 --- /dev/null +++ b/server/api/eoi/delete-generated-document.ts @@ -0,0 +1,131 @@ +import { getInterestById, updateInterest } from '~/server/utils/nocodb'; +import { checkDocumentSignatureStatus } from '~/server/utils/documeso'; +import type { InterestSalesProcessLevel, EOIStatus } from '~/utils/types'; + +export default defineEventHandler(async (event) => { + const xTagHeader = getRequestHeader(event, "x-tag"); + + console.log('[Delete Generated EOI] Request received with x-tag:', xTagHeader); + + if (!xTagHeader || (xTagHeader !== "094ut234" && xTagHeader !== "pjnvü1230")) { + console.error('[Delete Generated EOI] Authentication failed - invalid x-tag'); + throw createError({ statusCode: 401, statusMessage: "unauthenticated" }); + } + + try { + const body = await readBody(event); + const { interestId } = body; + + console.log('[Delete Generated EOI] Interest ID:', interestId); + + if (!interestId) { + console.error('[Delete Generated EOI] 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', + }); + } + + const documensoID = interest['documensoID']; + console.log('[Delete Generated EOI] Documenso ID:', documensoID); + + if (!documensoID) { + throw createError({ + statusCode: 400, + statusMessage: 'No generated document found to delete', + }); + } + + // Check if document is fully signed - prevent deletion if it is + try { + const signatureStatus = await checkDocumentSignatureStatus(parseInt(documensoID)); + console.log('[Delete Generated EOI] Signature status:', signatureStatus); + + if (signatureStatus.allSigned) { + throw createError({ + statusCode: 400, + statusMessage: 'Cannot delete a fully signed document. All parties have already signed.', + }); + } + } catch (error: any) { + console.error('[Delete Generated EOI] Failed to check signature status:', error); + // If we can't check status, we'll proceed with deletion but warn + console.warn('[Delete Generated EOI] Proceeding with deletion despite signature status check failure'); + } + + // Delete document from Documenso + const documensoApiKey = process.env.NUXT_DOCUMENSO_API_KEY; + const documensoBaseUrl = process.env.NUXT_DOCUMENSO_BASE_URL; + + if (!documensoApiKey || !documensoBaseUrl) { + throw createError({ + statusCode: 500, + statusMessage: "Documenso configuration missing. Please check NUXT_DOCUMENSO_API_KEY and NUXT_DOCUMENSO_BASE_URL environment variables." + }); + } + + console.log('[Delete Generated EOI] Deleting document from Documenso'); + try { + const deleteResponse = await fetch(`${documensoBaseUrl}/api/v1/documents/${documensoID}`, { + method: 'DELETE', + headers: { + 'Authorization': `Bearer ${documensoApiKey}`, + 'Content-Type': 'application/json' + } + }); + + if (!deleteResponse.ok) { + const errorText = await deleteResponse.text(); + console.error('[Delete Generated EOI] Documenso deletion failed:', errorText); + throw new Error(`Failed to delete document from Documenso: ${deleteResponse.statusText}`); + } + + console.log('[Delete Generated EOI] Successfully deleted document from Documenso'); + } catch (error: any) { + console.error('[Delete Generated EOI] Documenso deletion error:', error); + throw createError({ + statusCode: 500, + statusMessage: `Failed to delete document from Documenso: ${error.message}`, + }); + } + + // Reset interest fields + const updateData = { + 'EOI Status': 'Awaiting Further Details' as EOIStatus, + 'Sales Process Level': 'Specific Qualified Interest' as InterestSalesProcessLevel, + 'EOI Time Sent': undefined, + 'Signature Link Client': undefined, + 'Signature Link CC': undefined, + 'Signature Link Developer': undefined, + 'documensoID': undefined, + 'reminder_enabled': false + }; + + console.log('[Delete Generated EOI] Resetting interest fields'); + + // Update the interest + await updateInterest(interestId, updateData); + + console.log('[Delete Generated EOI] Delete completed successfully'); + return { + success: true, + message: 'Generated EOI document deleted successfully', + }; + } catch (error: any) { + console.error('[Delete Generated EOI] Failed to delete generated EOI document:', error); + console.error('[Delete Generated EOI] Error stack:', error.stack); + throw createError({ + statusCode: error.statusCode || 500, + statusMessage: error.statusMessage || error.message || 'Failed to delete generated EOI document', + }); + } +}); diff --git a/utils/types.ts b/utils/types.ts index 66ebcda..436ae13 100644 --- a/utils/types.ts +++ b/utils/types.ts @@ -127,6 +127,8 @@ export interface Interest { "Signature Link Client"?: string; "Signature Link CC"?: string; "Signature Link Developer"?: string; + // Documenso document ID for generated documents + "documensoID"?: string; } export interface InterestsResponse {