This commit is contained in:
2025-06-11 13:50:51 +02:00
parent e5b8affa84
commit a49322f852
3 changed files with 571 additions and 75 deletions

View File

@@ -31,7 +31,7 @@
<!-- Generate EOI Button - Only show if no documents uploaded -->
<div v-if="!hasEOI && !hasEOIDocuments" class="d-flex flex-wrap gap-2">
<v-btn
@click="generateEOI"
@click="generateEOI(0, false)"
:loading="isGenerating"
color="primary"
variant="flat"
@@ -84,66 +84,112 @@
</div>
<!-- Signature Links - Only show if EOI is generated but not uploaded/signed -->
<v-list v-if="hasEOI && !isEOISigned" :density="mobile ? 'compact' : 'comfortable'">
<v-list-item class="mb-2">
<template v-slot:prepend>
<v-avatar color="primary" :size="mobile ? 32 : 40">
<v-icon :size="mobile ? 'small' : 'default'">mdi-account</v-icon>
</v-avatar>
</template>
<v-list-item-title :class="mobile ? 'text-body-2' : ''">Client Signature Link</v-list-item-title>
<v-list-item-subtitle :class="mobile ? 'text-caption' : ''">{{ interest['Full Name'] }}</v-list-item-subtitle>
<template v-slot:append>
<v-btn
icon="mdi-content-copy"
variant="text"
:size="mobile ? 'small' : 'default'"
@click="copyLink(interest['Signature Link Client'])"
></v-btn>
</template>
</v-list-item>
<div v-if="hasEOI && !isEOISigned">
<div class="d-flex align-center mb-3">
<v-chip color="info" variant="tonal" size="small" prepend-icon="mdi-account-multiple">
Signature Status
</v-chip>
<v-btn
icon="mdi-refresh"
variant="text"
size="small"
:loading="isCheckingStatus"
@click="checkSignatureStatus"
class="ml-2"
></v-btn>
</div>
<v-list :density="mobile ? 'compact' : 'comfortable'">
<v-list-item class="mb-2">
<template v-slot:prepend>
<v-avatar color="primary" :size="mobile ? 32 : 40">
<v-icon :size="mobile ? 'small' : 'default'">mdi-account</v-icon>
</v-avatar>
</template>
<v-list-item-title :class="mobile ? 'text-body-2' : ''">Client Signature Link</v-list-item-title>
<v-list-item-subtitle :class="mobile ? 'text-caption' : ''">{{ interest['Full Name'] }}</v-list-item-subtitle>
<template v-slot:append>
<div class="d-flex align-center">
<v-icon
v-if="getSignatureStatusIcon('client')"
:color="getSignatureStatusColor('client')"
:size="mobile ? 'small' : 'default'"
class="mr-2"
>
{{ getSignatureStatusIcon('client') }}
</v-icon>
<v-btn
icon="mdi-content-copy"
variant="text"
:size="mobile ? 'small' : 'default'"
@click="copyLink(interest['Signature Link Client'])"
></v-btn>
</div>
</template>
</v-list-item>
<v-list-item class="mb-2">
<template v-slot:prepend>
<v-avatar color="success" :size="mobile ? 32 : 40">
<v-icon :size="mobile ? 'small' : 'default'">mdi-account-check</v-icon>
</v-avatar>
</template>
<v-list-item-title :class="mobile ? 'text-body-2' : ''">CC Signature Link</v-list-item-title>
<v-list-item-subtitle :class="mobile ? 'text-caption' : ''">Oscar Faragher</v-list-item-subtitle>
<template v-slot:append>
<v-btn
icon="mdi-content-copy"
variant="text"
:size="mobile ? 'small' : 'default'"
@click="copyLink(interest['Signature Link CC'])"
></v-btn>
</template>
</v-list-item>
<v-list-item class="mb-2">
<template v-slot:prepend>
<v-avatar color="success" :size="mobile ? 32 : 40">
<v-icon :size="mobile ? 'small' : 'default'">mdi-account-check</v-icon>
</v-avatar>
</template>
<v-list-item-title :class="mobile ? 'text-body-2' : ''">CC Signature Link</v-list-item-title>
<v-list-item-subtitle :class="mobile ? 'text-caption' : ''">Oscar Faragher</v-list-item-subtitle>
<template v-slot:append>
<div class="d-flex align-center">
<v-icon
v-if="getSignatureStatusIcon('cc')"
:color="getSignatureStatusColor('cc')"
:size="mobile ? 'small' : 'default'"
class="mr-2"
>
{{ getSignatureStatusIcon('cc') }}
</v-icon>
<v-btn
icon="mdi-content-copy"
variant="text"
:size="mobile ? 'small' : 'default'"
@click="copyLink(interest['Signature Link CC'])"
></v-btn>
</div>
</template>
</v-list-item>
<v-list-item class="mb-2">
<template v-slot:prepend>
<v-avatar color="secondary" :size="mobile ? 32 : 40">
<v-icon :size="mobile ? 'small' : 'default'">mdi-account-tie</v-icon>
</v-avatar>
</template>
<v-list-item-title :class="mobile ? 'text-body-2' : ''">Developer Signature Link</v-list-item-title>
<v-list-item-subtitle :class="mobile ? 'text-caption' : ''">David Mizrahi</v-list-item-subtitle>
<template v-slot:append>
<v-btn
icon="mdi-content-copy"
variant="text"
:size="mobile ? 'small' : 'default'"
@click="copyLink(interest['Signature Link Developer'])"
></v-btn>
</template>
</v-list-item>
</v-list>
<v-list-item class="mb-2">
<template v-slot:prepend>
<v-avatar color="secondary" :size="mobile ? 32 : 40">
<v-icon :size="mobile ? 'small' : 'default'">mdi-account-tie</v-icon>
</v-avatar>
</template>
<v-list-item-title :class="mobile ? 'text-body-2' : ''">Developer Signature Link</v-list-item-title>
<v-list-item-subtitle :class="mobile ? 'text-caption' : ''">David Mizrahi</v-list-item-subtitle>
<template v-slot:append>
<div class="d-flex align-center">
<v-icon
v-if="getSignatureStatusIcon('developer')"
:color="getSignatureStatusColor('developer')"
:size="mobile ? 'small' : 'default'"
class="mr-2"
>
{{ getSignatureStatusIcon('developer') }}
</v-icon>
<v-btn
icon="mdi-content-copy"
variant="text"
:size="mobile ? 'small' : 'default'"
@click="copyLink(interest['Signature Link Developer'])"
></v-btn>
</div>
</template>
</v-list-item>
</v-list>
</div>
<!-- Regenerate Button - Only show if EOI is generated but not uploaded/signed -->
<div v-if="hasEOI && !isEOISigned" class="mt-4">
<!-- Regenerate and Delete Generated EOI Buttons - Only show if EOI is generated but not uploaded/signed -->
<div v-if="hasEOI && !isEOISigned" class="mt-4 d-flex flex-wrap gap-2">
<v-btn
@click="generateEOI"
@click="generateEOI(0, true)"
:loading="isGenerating"
variant="text"
size="small"
@@ -151,6 +197,17 @@
>
Regenerate EOI
</v-btn>
<v-btn
v-if="canDeleteGenerated"
@click="showDeleteGeneratedDialog = true"
color="error"
variant="outlined"
size="small"
prepend-icon="mdi-delete"
>
Delete Generated EOI
</v-btn>
</div>
<!-- Delete EOI Button - Only show if EOI is uploaded/signed -->
@@ -274,6 +331,103 @@
</v-card>
</v-dialog>
<!-- Delete Generated EOI Dialog with Slider Confirmation -->
<v-dialog
v-model="showDeleteGeneratedDialog"
:max-width="mobile ? '100%' : '500'"
:transition="mobile ? 'dialog-bottom-transition' : 'dialog-transition'"
persistent
>
<v-card>
<v-card-title class="d-flex align-center">
<v-icon class="mr-2" color="error">mdi-delete-alert</v-icon>
Delete Generated EOI Document
</v-card-title>
<v-card-text>
<div class="mb-4">
<v-alert type="warning" variant="tonal" class="mb-4">
<v-icon>mdi-alert-triangle</v-icon>
This will permanently delete the generated EOI document from Documenso.
</v-alert>
This action will:
<ul class="mt-2 mb-4">
<li>Delete the document from Documenso platform</li>
<li>Remove all signature links</li>
<li>Reset the Sales Process Level to "Specific Qualified Interest"</li>
<li>Reset the EOI Status to "Awaiting Further Details"</li>
<li>Reset the EOI Time Sent field</li>
<li>Allow a new EOI to be generated</li>
</ul>
<div v-if="signatureStatus?.allSigned" class="text-error mb-3">
<v-icon class="mr-1">mdi-block-helper</v-icon>
Cannot delete: All parties have already signed this document.
</div>
<div v-else-if="signatureStatus && (signatureStatus.signedRecipients?.length > 0)" class="text-warning mb-3">
<v-icon class="mr-1">mdi-alert</v-icon>
Warning: Some parties have already signed this document.
</div>
</div>
<div v-if="!signatureStatus?.allSigned">
<v-divider class="mb-4" />
<div class="text-subtitle-2 mb-3">
To confirm deletion, slide the handle all the way to the right:
</div>
<div class="position-relative">
<v-slider
v-model="deleteSliderValue"
:max="100"
:min="0"
color="error"
track-color="grey-lighten-2"
thumb-label="always"
class="mt-2"
:disabled="isDeletingGenerated"
>
<template v-slot:thumb-label="{ modelValue }">
{{ Math.round(modelValue) }}%
</template>
</v-slider>
<div class="d-flex justify-space-between text-caption mt-1">
<span>Cancel</span>
<span :class="deleteSliderValue >= 100 ? 'text-error font-weight-bold' : 'text-grey'">
DELETE
</span>
</div>
</div>
</div>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn
@click="closeDeleteGeneratedDialog"
variant="text"
:disabled="isDeletingGenerated"
>
Cancel
</v-btn>
<v-btn
color="error"
variant="flat"
@click="deleteGeneratedEOI"
:loading="isDeletingGenerated"
:disabled="deleteSliderValue < 100 || signatureStatus?.allSigned"
>
<v-icon class="mr-1">mdi-delete</v-icon>
Delete Generated EOI
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</template>
@@ -297,6 +451,11 @@ const isUploading = ref(false);
const selectedFile = ref<File | null>(null);
const showDeleteConfirmDialog = ref(false);
const isDeleting = ref(false);
const signatureStatus = ref<any>(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();
}
});
</script>