updates
This commit is contained in:
parent
0b6601fabc
commit
f992fbb5a6
|
|
@ -28,8 +28,8 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Generate EOI Button - Only show if no documents uploaded -->
|
<!-- Generate EOI Button - Only show if no documents uploaded and no generated EOI -->
|
||||||
<div v-if="!hasEOI && !hasEOIDocuments" class="d-flex flex-wrap gap-2">
|
<div v-if="!hasGeneratedEOI && !hasEOIDocuments" class="d-flex flex-wrap gap-2">
|
||||||
<v-btn
|
<v-btn
|
||||||
@click="generateEOI"
|
@click="generateEOI"
|
||||||
:loading="isGenerating"
|
:loading="isGenerating"
|
||||||
|
|
@ -53,8 +53,29 @@
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Upload EOI Button - Only show if EOI exists or has documents -->
|
<!-- Message when uploaded documents exist -->
|
||||||
<div v-else-if="hasEOI || hasEOIDocuments" class="mb-4">
|
<div v-else-if="hasEOIDocuments && !hasGeneratedEOI" class="mb-4">
|
||||||
|
<v-alert
|
||||||
|
type="info"
|
||||||
|
variant="tonal"
|
||||||
|
class="mb-3"
|
||||||
|
>
|
||||||
|
Uploaded EOI documents found. Remove uploaded documents to generate a new EOI.
|
||||||
|
</v-alert>
|
||||||
|
<v-btn
|
||||||
|
@click="showUploadDialog = true"
|
||||||
|
variant="tonal"
|
||||||
|
color="success"
|
||||||
|
prepend-icon="mdi-upload"
|
||||||
|
:disabled="isUploading"
|
||||||
|
:size="mobile ? 'default' : 'large'"
|
||||||
|
>
|
||||||
|
Upload Additional EOI
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Upload EOI Button - Show if generated EOI exists -->
|
||||||
|
<div v-else-if="hasGeneratedEOI" class="mb-4">
|
||||||
<v-btn
|
<v-btn
|
||||||
@click="showUploadDialog = true"
|
@click="showUploadDialog = true"
|
||||||
variant="tonal"
|
variant="tonal"
|
||||||
|
|
@ -68,7 +89,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- EOI Status Badge -->
|
<!-- EOI Status Badge -->
|
||||||
<div v-if="hasEOI || hasEOIDocuments" class="mb-4 d-flex align-center flex-wrap">
|
<div v-if="hasGeneratedEOI || hasEOIDocuments" class="mb-4 d-flex align-center flex-wrap">
|
||||||
<v-chip
|
<v-chip
|
||||||
:color="getStatusColor(interest['EOI Status'])"
|
:color="getStatusColor(interest['EOI Status'])"
|
||||||
variant="tonal"
|
variant="tonal"
|
||||||
|
|
@ -83,15 +104,41 @@
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Signature Links - Only show if EOI is generated but not uploaded/signed -->
|
<!-- Signature Status Section - Only show if EOI is generated but not signed -->
|
||||||
<v-list v-if="hasEOI && !isEOISigned" :density="mobile ? 'compact' : 'comfortable'">
|
<div v-if="hasGeneratedEOI && !isEOISigned" class="mb-4">
|
||||||
|
<div class="text-subtitle-2 mb-2 d-flex align-center">
|
||||||
|
Signature Status
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-refresh"
|
||||||
|
variant="text"
|
||||||
|
size="x-small"
|
||||||
|
class="ml-2"
|
||||||
|
@click="checkSignatureStatus"
|
||||||
|
:loading="isCheckingSignatures"
|
||||||
|
></v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-list :density="mobile ? 'compact' : 'comfortable'">
|
||||||
<v-list-item class="mb-2">
|
<v-list-item class="mb-2">
|
||||||
<template v-slot:prepend>
|
<template v-slot:prepend>
|
||||||
<v-avatar color="primary" :size="mobile ? 32 : 40">
|
<v-avatar :color="getSignatureStatusColor('client')" :size="mobile ? 32 : 40">
|
||||||
<v-icon :size="mobile ? 'small' : 'default'">mdi-account</v-icon>
|
<v-icon :size="mobile ? 'small' : 'default'">
|
||||||
|
{{ getSignatureStatusIcon('client') }}
|
||||||
|
</v-icon>
|
||||||
</v-avatar>
|
</v-avatar>
|
||||||
</template>
|
</template>
|
||||||
<v-list-item-title :class="mobile ? 'text-body-2' : ''">Client Signature Link</v-list-item-title>
|
<v-list-item-title :class="mobile ? 'text-body-2' : ''">
|
||||||
|
Client Signature
|
||||||
|
<v-chip
|
||||||
|
v-if="signatureStatus"
|
||||||
|
:color="getSignatureStatusColor('client')"
|
||||||
|
size="x-small"
|
||||||
|
variant="tonal"
|
||||||
|
class="ml-2"
|
||||||
|
>
|
||||||
|
{{ getSignatureStatusText('client') }}
|
||||||
|
</v-chip>
|
||||||
|
</v-list-item-title>
|
||||||
<v-list-item-subtitle :class="mobile ? 'text-caption' : ''">{{ interest['Full Name'] }}</v-list-item-subtitle>
|
<v-list-item-subtitle :class="mobile ? 'text-caption' : ''">{{ interest['Full Name'] }}</v-list-item-subtitle>
|
||||||
<template v-slot:append>
|
<template v-slot:append>
|
||||||
<v-btn
|
<v-btn
|
||||||
|
|
@ -105,11 +152,24 @@
|
||||||
|
|
||||||
<v-list-item class="mb-2">
|
<v-list-item class="mb-2">
|
||||||
<template v-slot:prepend>
|
<template v-slot:prepend>
|
||||||
<v-avatar color="success" :size="mobile ? 32 : 40">
|
<v-avatar :color="getSignatureStatusColor('cc')" :size="mobile ? 32 : 40">
|
||||||
<v-icon :size="mobile ? 'small' : 'default'">mdi-account-check</v-icon>
|
<v-icon :size="mobile ? 'small' : 'default'">
|
||||||
|
{{ getSignatureStatusIcon('cc') }}
|
||||||
|
</v-icon>
|
||||||
</v-avatar>
|
</v-avatar>
|
||||||
</template>
|
</template>
|
||||||
<v-list-item-title :class="mobile ? 'text-body-2' : ''">CC Signature Link</v-list-item-title>
|
<v-list-item-title :class="mobile ? 'text-body-2' : ''">
|
||||||
|
CC Signature
|
||||||
|
<v-chip
|
||||||
|
v-if="signatureStatus"
|
||||||
|
:color="getSignatureStatusColor('cc')"
|
||||||
|
size="x-small"
|
||||||
|
variant="tonal"
|
||||||
|
class="ml-2"
|
||||||
|
>
|
||||||
|
{{ getSignatureStatusText('cc') }}
|
||||||
|
</v-chip>
|
||||||
|
</v-list-item-title>
|
||||||
<v-list-item-subtitle :class="mobile ? 'text-caption' : ''">Oscar Faragher</v-list-item-subtitle>
|
<v-list-item-subtitle :class="mobile ? 'text-caption' : ''">Oscar Faragher</v-list-item-subtitle>
|
||||||
<template v-slot:append>
|
<template v-slot:append>
|
||||||
<v-btn
|
<v-btn
|
||||||
|
|
@ -123,11 +183,24 @@
|
||||||
|
|
||||||
<v-list-item class="mb-2">
|
<v-list-item class="mb-2">
|
||||||
<template v-slot:prepend>
|
<template v-slot:prepend>
|
||||||
<v-avatar color="secondary" :size="mobile ? 32 : 40">
|
<v-avatar :color="getSignatureStatusColor('developer')" :size="mobile ? 32 : 40">
|
||||||
<v-icon :size="mobile ? 'small' : 'default'">mdi-account-tie</v-icon>
|
<v-icon :size="mobile ? 'small' : 'default'">
|
||||||
|
{{ getSignatureStatusIcon('developer') }}
|
||||||
|
</v-icon>
|
||||||
</v-avatar>
|
</v-avatar>
|
||||||
</template>
|
</template>
|
||||||
<v-list-item-title :class="mobile ? 'text-body-2' : ''">Developer Signature Link</v-list-item-title>
|
<v-list-item-title :class="mobile ? 'text-body-2' : ''">
|
||||||
|
Developer Signature
|
||||||
|
<v-chip
|
||||||
|
v-if="signatureStatus"
|
||||||
|
:color="getSignatureStatusColor('developer')"
|
||||||
|
size="x-small"
|
||||||
|
variant="tonal"
|
||||||
|
class="ml-2"
|
||||||
|
>
|
||||||
|
{{ getSignatureStatusText('developer') }}
|
||||||
|
</v-chip>
|
||||||
|
</v-list-item-title>
|
||||||
<v-list-item-subtitle :class="mobile ? 'text-caption' : ''">David Mizrahi</v-list-item-subtitle>
|
<v-list-item-subtitle :class="mobile ? 'text-caption' : ''">David Mizrahi</v-list-item-subtitle>
|
||||||
<template v-slot:append>
|
<template v-slot:append>
|
||||||
<v-btn
|
<v-btn
|
||||||
|
|
@ -139,9 +212,10 @@
|
||||||
</template>
|
</template>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
</v-list>
|
</v-list>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Regenerate Button - Only show if EOI is generated but not uploaded/signed -->
|
<!-- Action Buttons Section -->
|
||||||
<div v-if="hasEOI && !isEOISigned" class="mt-4">
|
<div v-if="hasGeneratedEOI && !isEOISigned" class="mt-4 d-flex flex-wrap gap-2">
|
||||||
<v-btn
|
<v-btn
|
||||||
@click="generateEOI"
|
@click="generateEOI"
|
||||||
:loading="isGenerating"
|
:loading="isGenerating"
|
||||||
|
|
@ -151,6 +225,17 @@
|
||||||
>
|
>
|
||||||
Regenerate EOI
|
Regenerate EOI
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
@click="showDeleteGeneratedConfirmDialog = true"
|
||||||
|
color="error"
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
prepend-icon="mdi-delete"
|
||||||
|
:disabled="signatureStatus?.allSigned"
|
||||||
|
>
|
||||||
|
Delete Generated EOI
|
||||||
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Delete EOI Button - Only show if EOI is uploaded/signed -->
|
<!-- Delete EOI Button - Only show if EOI is uploaded/signed -->
|
||||||
|
|
@ -162,7 +247,7 @@
|
||||||
size="small"
|
size="small"
|
||||||
prepend-icon="mdi-delete"
|
prepend-icon="mdi-delete"
|
||||||
>
|
>
|
||||||
Delete EOI
|
Delete Uploaded EOI
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -231,7 +316,7 @@
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
|
|
||||||
<!-- Delete Confirmation Dialog -->
|
<!-- Delete Confirmation Dialog for Uploaded Documents -->
|
||||||
<v-dialog
|
<v-dialog
|
||||||
v-model="showDeleteConfirmDialog"
|
v-model="showDeleteConfirmDialog"
|
||||||
:max-width="mobile ? '100%' : '400'"
|
:max-width="mobile ? '100%' : '400'"
|
||||||
|
|
@ -244,7 +329,7 @@
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
|
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
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:
|
||||||
<ul class="mt-2">
|
<ul class="mt-2">
|
||||||
<li>Remove the uploaded EOI document</li>
|
<li>Remove the uploaded EOI document</li>
|
||||||
<li>Reset the Sales Process Level to "Specific Qualified Interest"</li>
|
<li>Reset the Sales Process Level to "Specific Qualified Interest"</li>
|
||||||
|
|
@ -265,7 +350,7 @@
|
||||||
<v-btn
|
<v-btn
|
||||||
color="error"
|
color="error"
|
||||||
variant="flat"
|
variant="flat"
|
||||||
@click="deleteEOI"
|
@click="deleteUploadedEOI"
|
||||||
:loading="isDeleting"
|
:loading="isDeleting"
|
||||||
>
|
>
|
||||||
Delete EOI
|
Delete EOI
|
||||||
|
|
@ -274,6 +359,27 @@
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
|
|
||||||
|
<!-- Slider Confirmation Dialog for Generated Documents -->
|
||||||
|
<SliderConfirmation
|
||||||
|
v-model="showDeleteGeneratedConfirmDialog"
|
||||||
|
title="Delete Generated EOI"
|
||||||
|
message="You are about to delete the generated EOI document. This will permanently remove the document from Documenso and reset all signature links."
|
||||||
|
icon="mdi-delete"
|
||||||
|
iconColor="error"
|
||||||
|
confirmButtonText="Delete EOI"
|
||||||
|
sliderText="Slide to delete generated EOI"
|
||||||
|
warningText="This action cannot be undone and will require regenerating the document."
|
||||||
|
:confirmationList="[
|
||||||
|
'Delete document from Documenso',
|
||||||
|
'Remove all signature links',
|
||||||
|
'Reset EOI status to \'Awaiting Further Details\'',
|
||||||
|
'Reset Sales Process Level to \'Specific Qualified Interest\''
|
||||||
|
]"
|
||||||
|
:loading="isDeletingGenerated"
|
||||||
|
@confirm="deleteGeneratedEOI"
|
||||||
|
@cancel="showDeleteGeneratedConfirmDialog = false"
|
||||||
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -296,12 +402,17 @@ const showUploadDialog = ref(false);
|
||||||
const isUploading = ref(false);
|
const isUploading = ref(false);
|
||||||
const selectedFile = ref<File | null>(null);
|
const selectedFile = ref<File | null>(null);
|
||||||
const showDeleteConfirmDialog = ref(false);
|
const showDeleteConfirmDialog = ref(false);
|
||||||
|
const showDeleteGeneratedConfirmDialog = ref(false);
|
||||||
const isDeleting = ref(false);
|
const isDeleting = ref(false);
|
||||||
|
const isDeletingGenerated = ref(false);
|
||||||
|
const signatureStatus = ref<any>(null);
|
||||||
|
const isCheckingSignatures = ref(false);
|
||||||
|
|
||||||
const hasEOI = computed(() => {
|
const hasGeneratedEOI = computed(() => {
|
||||||
return !!(props.interest['Signature Link Client'] ||
|
return !!(props.interest['Signature Link Client'] ||
|
||||||
props.interest['Signature Link CC'] ||
|
props.interest['Signature Link CC'] ||
|
||||||
props.interest['Signature Link Developer']);
|
props.interest['Signature Link Developer'] ||
|
||||||
|
props.interest['documensoID']);
|
||||||
});
|
});
|
||||||
|
|
||||||
const eoiDocuments = computed(() => {
|
const eoiDocuments = computed(() => {
|
||||||
|
|
@ -342,6 +453,11 @@ const generateEOI = async (retryCount = 0) => {
|
||||||
|
|
||||||
emit('eoi-generated', { signingLinks: response.signingLinks });
|
emit('eoi-generated', { signingLinks: response.signingLinks });
|
||||||
emit('update'); // Trigger parent to refresh data
|
emit('update'); // Trigger parent to refresh data
|
||||||
|
|
||||||
|
// Check signature status after generation
|
||||||
|
if (response.documentId !== 'existing') {
|
||||||
|
setTimeout(() => checkSignatureStatus(), 1000);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error('EOI generation failed');
|
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<any>('/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) => {
|
const copyLink = async (link: string | undefined) => {
|
||||||
if (!link) return;
|
if (!link) return;
|
||||||
|
|
||||||
|
|
@ -473,13 +653,12 @@ const handleUpload = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const closeUploadDialog = () => {
|
const closeUploadDialog = () => {
|
||||||
showUploadDialog.value = false;
|
showUploadDialog.value = false;
|
||||||
selectedFile.value = null;
|
selectedFile.value = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteEOI = async () => {
|
const deleteUploadedEOI = async () => {
|
||||||
isDeleting.value = true;
|
isDeleting.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -494,15 +673,50 @@ const deleteEOI = async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
toast.success('EOI deleted successfully');
|
toast.success('Uploaded EOI deleted successfully');
|
||||||
showDeleteConfirmDialog.value = false;
|
showDeleteConfirmDialog.value = false;
|
||||||
emit('update'); // Refresh parent data
|
emit('update'); // Refresh parent data
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Failed to delete EOI:', error);
|
console.error('Failed to delete uploaded EOI:', error);
|
||||||
toast.error(error.data?.statusMessage || 'Failed to delete EOI');
|
toast.error(error.data?.statusMessage || 'Failed to delete uploaded EOI');
|
||||||
} finally {
|
} finally {
|
||||||
isDeleting.value = false;
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,251 @@
|
||||||
|
<template>
|
||||||
|
<v-dialog
|
||||||
|
v-model="showDialog"
|
||||||
|
:max-width="mobile ? '100%' : '500'"
|
||||||
|
:fullscreen="mobile"
|
||||||
|
:transition="mobile ? 'dialog-bottom-transition' : 'dialog-transition'"
|
||||||
|
persistent
|
||||||
|
>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="d-flex align-center">
|
||||||
|
<v-icon class="mr-2" :color="iconColor">{{ icon }}</v-icon>
|
||||||
|
{{ title }}
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-close"
|
||||||
|
variant="text"
|
||||||
|
@click="cancel"
|
||||||
|
:disabled="isSliding"
|
||||||
|
></v-btn>
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-divider />
|
||||||
|
|
||||||
|
<v-card-text :class="mobile ? 'pa-4' : 'pa-6'">
|
||||||
|
<div :class="mobile ? 'text-body-2 mb-4' : 'text-body-1 mb-6'">
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="confirmationList && confirmationList.length > 0" class="mb-4">
|
||||||
|
<ul class="text-caption">
|
||||||
|
<li v-for="item in confirmationList" :key="item">{{ item }}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-error mb-4" :class="mobile ? 'text-caption' : 'text-body-2'">
|
||||||
|
{{ warningText }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Slider Track -->
|
||||||
|
<div class="slider-container mb-4">
|
||||||
|
<div class="slider-track" ref="sliderTrack">
|
||||||
|
<div
|
||||||
|
class="slider-fill"
|
||||||
|
:style="{ width: `${sliderProgress}%` }"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="slider-handle"
|
||||||
|
:style="{ left: `${sliderProgress}%` }"
|
||||||
|
@mousedown="startSliding"
|
||||||
|
@touchstart="startSliding"
|
||||||
|
>
|
||||||
|
<v-icon size="20" color="white">
|
||||||
|
{{ sliderProgress >= 100 ? 'mdi-check' : 'mdi-chevron-right' }}
|
||||||
|
</v-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="slider-text" :class="mobile ? 'text-caption' : 'text-body-2'">
|
||||||
|
{{ sliderProgress >= 100 ? 'Ready to delete!' : sliderText }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-divider />
|
||||||
|
|
||||||
|
<v-card-actions class="pa-4">
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
@click="cancel"
|
||||||
|
variant="text"
|
||||||
|
:size="mobile ? 'default' : 'large'"
|
||||||
|
:disabled="isSliding"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="error"
|
||||||
|
variant="flat"
|
||||||
|
@click="confirm"
|
||||||
|
:loading="isConfirming"
|
||||||
|
:disabled="sliderProgress < 100"
|
||||||
|
:size="mobile ? 'default' : 'large'"
|
||||||
|
>
|
||||||
|
{{ confirmButtonText }}
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
interface Props {
|
||||||
|
modelValue: boolean;
|
||||||
|
title?: string;
|
||||||
|
message?: string;
|
||||||
|
icon?: string;
|
||||||
|
iconColor?: string;
|
||||||
|
confirmButtonText?: string;
|
||||||
|
sliderText?: string;
|
||||||
|
warningText?: string;
|
||||||
|
confirmationList?: string[];
|
||||||
|
loading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
title: 'Confirm Action',
|
||||||
|
message: 'Are you sure you want to proceed?',
|
||||||
|
icon: 'mdi-alert',
|
||||||
|
iconColor: 'warning',
|
||||||
|
confirmButtonText: 'Confirm',
|
||||||
|
sliderText: 'Slide to confirm',
|
||||||
|
warningText: 'This action cannot be undone.',
|
||||||
|
confirmationList: () => [],
|
||||||
|
loading: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: boolean];
|
||||||
|
'confirm': [];
|
||||||
|
'cancel': [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { mobile } = useDisplay();
|
||||||
|
|
||||||
|
const showDialog = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (value) => emit('update:modelValue', value)
|
||||||
|
});
|
||||||
|
|
||||||
|
const isConfirming = computed(() => props.loading);
|
||||||
|
|
||||||
|
// Slider state
|
||||||
|
const sliderProgress = ref(0);
|
||||||
|
const isSliding = ref(false);
|
||||||
|
const sliderTrack = ref<HTMLElement>();
|
||||||
|
|
||||||
|
const startSliding = (event: MouseEvent | TouchEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
isSliding.value = true;
|
||||||
|
|
||||||
|
const handleMove = (moveEvent: MouseEvent | TouchEvent) => {
|
||||||
|
if (!sliderTrack.value || !isSliding.value) return;
|
||||||
|
|
||||||
|
const rect = sliderTrack.value.getBoundingClientRect();
|
||||||
|
const clientX = 'touches' in moveEvent ? moveEvent.touches[0].clientX : moveEvent.clientX;
|
||||||
|
const progress = Math.max(0, Math.min(100, ((clientX - rect.left) / rect.width) * 100));
|
||||||
|
|
||||||
|
sliderProgress.value = progress;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEnd = () => {
|
||||||
|
isSliding.value = false;
|
||||||
|
if (sliderProgress.value < 100) {
|
||||||
|
// Snap back if not fully slid
|
||||||
|
sliderProgress.value = 0;
|
||||||
|
}
|
||||||
|
document.removeEventListener('mousemove', handleMove);
|
||||||
|
document.removeEventListener('mouseup', handleEnd);
|
||||||
|
document.removeEventListener('touchmove', handleMove);
|
||||||
|
document.removeEventListener('touchend', handleEnd);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', handleMove);
|
||||||
|
document.addEventListener('mouseup', handleEnd);
|
||||||
|
document.addEventListener('touchmove', handleMove);
|
||||||
|
document.addEventListener('touchend', handleEnd);
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirm = () => {
|
||||||
|
if (sliderProgress.value >= 100) {
|
||||||
|
emit('confirm');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancel = () => {
|
||||||
|
if (!isSliding.value) {
|
||||||
|
sliderProgress.value = 0;
|
||||||
|
emit('cancel');
|
||||||
|
showDialog.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reset slider when dialog is closed
|
||||||
|
watch(showDialog, (newValue) => {
|
||||||
|
if (!newValue) {
|
||||||
|
sliderProgress.value = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.slider-container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-track {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 50px;
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
border-radius: 25px;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-fill {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #ff5722, #d32f2f);
|
||||||
|
border-radius: 25px;
|
||||||
|
transition: width 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-handle {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||||
|
cursor: grab;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
transition: transform 0.1s ease;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-handle:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
transform: translateX(-50%) scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-text {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 8px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Prevent text selection during sliding */
|
||||||
|
.slider-container * {
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -43,6 +43,27 @@ export default defineEventHandler(async (event) => {
|
||||||
throw createError({ statusCode: 404, statusMessage: "Interest not found" });
|
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)
|
// Check if EOI already exists (has signature links)
|
||||||
if (interest['Signature Link Client'] && interest['Signature Link CC'] && interest['Signature Link Developer']) {
|
if (interest['Signature Link Client'] && interest['Signature Link CC'] && interest['Signature Link Developer']) {
|
||||||
console.log('EOI already exists, returning existing links');
|
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
|
// Validate required fields
|
||||||
const requiredFields = [
|
const requiredFields = [
|
||||||
{ field: 'Full Name', value: interest['Full Name'] },
|
{ field: 'Full Name', value: interest['Full Name'] },
|
||||||
|
|
@ -104,18 +148,6 @@ export default defineEventHandler(async (event) => {
|
||||||
// Concatenate berth numbers
|
// Concatenate berth numbers
|
||||||
const berthNumbers = berths.map(b => b['Mooring Number']).join(', ');
|
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)
|
// 1. Get template (optional - just to verify it exists)
|
||||||
try {
|
try {
|
||||||
const templateResponse = await fetch(`${documensoBaseUrl}/api/v1/templates/${templateId}`, {
|
const templateResponse = await fetch(`${documensoBaseUrl}/api/v1/templates/${templateId}`, {
|
||||||
|
|
@ -272,7 +304,8 @@ export default defineEventHandler(async (event) => {
|
||||||
'EOI Status': 'Waiting for Signatures',
|
'EOI Status': 'Waiting for Signatures',
|
||||||
'Sales Process Level': 'LOI and NDA Sent',
|
'Sales Process Level': 'LOI and NDA Sent',
|
||||||
// Don't set EOI Time Sent here - only when email is sent or link is copied
|
// 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
|
// Add signing links to update data with new column names
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@ export default defineEventHandler(async (event) => {
|
||||||
'Signature Link CC': null,
|
'Signature Link CC': null,
|
||||||
'Signature Link Developer': null,
|
'Signature Link Developer': null,
|
||||||
'Documeso ID': null,
|
'Documeso ID': null,
|
||||||
|
'documensoID': null,
|
||||||
'reminder_enabled': false
|
'reminder_enabled': false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -127,6 +127,8 @@ export interface Interest {
|
||||||
"Signature Link Client"?: string;
|
"Signature Link Client"?: string;
|
||||||
"Signature Link CC"?: string;
|
"Signature Link CC"?: string;
|
||||||
"Signature Link Developer"?: string;
|
"Signature Link Developer"?: string;
|
||||||
|
// Documenso document ID for generated documents
|
||||||
|
"documensoID"?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InterestsResponse {
|
export interface InterestsResponse {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue