Add Documenso integration for EOI document generation

- Add Documenso API configuration to environment variables
- Create endpoint to generate EOI documents with e-signature capability
- Update email composer to insert generated EOI document links
- Add UI indicators for EOI generation status in interest details
- Emit events to refresh interest data after EOI generation
This commit is contained in:
2025-06-09 22:40:37 +02:00
parent 618c888b85
commit f7cc2973e1
7 changed files with 611 additions and 132 deletions

View File

@@ -32,6 +32,7 @@
<EmailComposer
:interest="interest"
@sent="onEmailSent"
@eoiGenerated="onEoiGenerated"
class="mb-4"
/>
@@ -56,7 +57,12 @@ interface Props {
interest: Interest;
}
interface Emits {
(e: 'interestUpdated'): void;
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
const isConnected = ref(false);
const connectedEmail = ref('');
@@ -83,6 +89,11 @@ const onEmailSent = (messageId: string) => {
threadView.value?.reloadEmails();
};
const onEoiGenerated = () => {
// Emit event to parent to refresh interest data
emit('interestUpdated');
};
const disconnect = () => {
// Clear session storage
sessionStorage.removeItem('emailSessionId');

View File

@@ -42,16 +42,18 @@
variant="outlined"
size="small"
@click="insertEOILink"
:disabled="sending"
:disabled="sending || generatingEOI"
:loading="generatingEOI"
>
<v-icon start>mdi-link</v-icon>
Insert EOI Link
{{ generatingEOI ? 'Generating...' : 'Insert EOI Link' }}
</v-btn>
<v-btn
variant="outlined"
size="small"
@click="insertFormLink"
:disabled="sending"
v-show="false"
>
<v-icon start>mdi-form-select</v-icon>
Insert Form Link
@@ -156,6 +158,7 @@ interface Props {
interface Emits {
(e: 'sent', messageId: string): void;
(e: 'eoiGenerated'): void;
}
const props = defineProps<Props>();
@@ -166,6 +169,7 @@ const toast = useToast();
const form = ref();
const sending = ref(false);
const generatingEOI = ref(false);
const showSignatureSettings = ref(false);
const includeSignature = ref(true);
@@ -192,11 +196,40 @@ const getSessionId = () => {
return sessionStorage.getItem('emailSessionId') || '';
};
const insertEOILink = () => {
// Generate EOI link similar to the existing EOI Send to Sales functionality
const eoiLink = `https://portnimara.com/eoi/${props.interest.Id}`;
email.value.body += `\n\nPlease click here to complete your Expression of Interest: ${eoiLink}\n`;
toast.success('EOI link inserted');
const insertEOILink = async () => {
// Check if we're already generating
if (generatingEOI.value) return;
generatingEOI.value = true;
try {
const response = await $fetch<{
success: boolean;
clientSigningUrl: string;
documentId: string;
}>('/api/email/generate-eoi-document', {
method: 'POST',
headers: {
'x-tag': user.value?.email ? '094ut234' : 'pjnvü1230',
},
body: {
interestId: props.interest.Id
}
});
if (response.success && response.clientSigningUrl) {
email.value.body += `\n\nPlease click here to sign your Letter of Intent: ${response.clientSigningUrl}\n`;
toast.success('EOI generated and link inserted!');
// Emit event to refresh interest data
emit('eoiGenerated');
}
} catch (error: any) {
console.error('Failed to generate EOI:', error);
toast.error(error.data?.statusMessage || 'Failed to generate EOI document');
} finally {
generatingEOI.value = false;
}
};
const insertFormLink = () => {
@@ -221,14 +254,12 @@ const getSignaturePreview = () => {
return `
<div style="margin-top: 20px;">
<div style="font-weight: bold;">${sig.name || 'Your Name'}</div>
<div style="color: #666;">${sig.title || 'Your Title'}</div>
<br>
<div style="font-weight: bold;">${sig.company || 'Company Name'}</div>
<br>
<div style="color: #666; margin-bottom: 8px;">${sig.title || 'Your Title'}</div>
<div style="font-weight: bold; margin-bottom: 12px;">${sig.company || 'Company Name'}</div>
${contactLines ? contactLines + '<br>' : ''}
<a href="mailto:${userEmail}" style="color: #0066cc;">${userEmail}</a>
<br><br>
<img src="${process.env.NUXT_EMAIL_LOGO_URL || 'https://portnimara.com/logo.png'}" alt="Logo" style="height: 80px;">
<img src="/Port_Nimara_Logo_2_Colour_New_Transparent.png" alt="Port Nimara" style="height: 60px; max-width: 200px;">
<br>
<div style="color: #666; font-size: 12px; margin-top: 10px;">
The information in this message is confidential and may be privileged.<br>

View File

@@ -626,10 +626,86 @@
</v-card-text>
</v-card>
<!-- EOI Links Section (only shows if EOI has been sent) -->
<v-card
v-if="hasEOILinks"
variant="flat"
class="mb-6"
>
<v-card-title class="text-h6 d-flex align-center pb-4">
<v-icon class="mr-2" color="primary">mdi-link-variant</v-icon>
EOI Links
</v-card-title>
<v-card-text class="pt-2">
<v-list>
<v-list-item
v-if="(interest as any)['EOI Client Link']"
class="mb-2"
>
<template v-slot:prepend>
<v-avatar color="primary" size="40">
<v-icon>mdi-account</v-icon>
</v-avatar>
</template>
<v-list-item-title>Client ({{ interest['Full Name'] }})</v-list-item-title>
<v-list-item-subtitle class="text-truncate">{{ (interest as any)['EOI Client Link'] }}</v-list-item-subtitle>
<template v-slot:append>
<v-btn
icon="mdi-content-copy"
variant="text"
@click="copyToClipboard((interest as any)['EOI Client Link'], 'Client')"
></v-btn>
</template>
</v-list-item>
<v-list-item
v-if="(interest as any)['EOI Oscar Link']"
class="mb-2"
>
<template v-slot:prepend>
<v-avatar color="success" size="40">
<v-icon>mdi-account-check</v-icon>
</v-avatar>
</template>
<v-list-item-title>Oscar Faragher (Approver)</v-list-item-title>
<v-list-item-subtitle class="text-truncate">{{ (interest as any)['EOI Oscar Link'] }}</v-list-item-subtitle>
<template v-slot:append>
<v-btn
icon="mdi-content-copy"
variant="text"
@click="copyToClipboard((interest as any)['EOI Oscar Link'], 'Oscar Faragher')"
></v-btn>
</template>
</v-list-item>
<v-list-item
v-if="(interest as any)['EOI David Link']"
class="mb-2"
>
<template v-slot:prepend>
<v-avatar color="secondary" size="40">
<v-icon>mdi-account-tie</v-icon>
</v-avatar>
</template>
<v-list-item-title>David Mizrahi (Signer)</v-list-item-title>
<v-list-item-subtitle class="text-truncate">{{ (interest as any)['EOI David Link'] }}</v-list-item-subtitle>
<template v-slot:append>
<v-btn
icon="mdi-content-copy"
variant="text"
@click="copyToClipboard((interest as any)['EOI David Link'], 'David Mizrahi')"
></v-btn>
</template>
</v-list-item>
</v-list>
</v-card-text>
</v-card>
<!-- Email Communication Section -->
<EmailCommunication
v-if="interest"
:interest="interest"
@interestUpdated="onInterestUpdated"
/>
</v-form>
</v-card-text>
@@ -723,6 +799,15 @@ const currentStep = computed(() => {
);
});
const hasEOILinks = computed(() => {
if (!interest.value) return false;
return !!(
(interest.value as any)['EOI Client Link'] ||
(interest.value as any)['EOI Oscar Link'] ||
(interest.value as any)['EOI David Link']
);
});
const closeModal = () => {
isOpen.value = false;
};
@@ -1092,6 +1177,41 @@ const deleteInterest = async () => {
}
};
// Copy to clipboard function
const copyToClipboard = async (text: string, recipient: string) => {
try {
await navigator.clipboard.writeText(text);
toast.success(`${recipient} link copied to clipboard!`);
} catch (err) {
console.error('Failed to copy text: ', err);
toast.error('Failed to copy link to clipboard');
}
};
// Handle interest updated event from EmailCommunication
const onInterestUpdated = async () => {
// Reload the interest data
if (interest.value) {
try {
const updatedInterest = await $fetch<Interest>(`/api/get-interest-by-id`, {
headers: {
"x-tag": user.value?.email ? "094ut234" : "pjnvü1230",
},
params: {
id: interest.value.Id,
},
});
if (updatedInterest) {
interest.value = { ...updatedInterest };
emit("save", interest.value); // Trigger parent refresh
}
} catch (error) {
console.error('Failed to reload interest:', error);
}
}
};
// Load berths when component mounts
onMounted(() => {
loadAvailableBerths();