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:
@@ -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');
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user