2025-06-10 00:37:43 +02:00
|
|
|
<template>
|
2025-06-10 00:42:14 +02:00
|
|
|
<div>
|
|
|
|
|
<v-card-title class="text-h6 d-flex align-center pb-4">
|
|
|
|
|
<v-icon class="mr-2" color="primary">mdi-file-document-edit</v-icon>
|
|
|
|
|
EOI Management
|
|
|
|
|
</v-card-title>
|
2025-06-10 12:54:22 +02:00
|
|
|
<v-card-text class="pt-0">
|
2025-06-10 13:59:09 +02:00
|
|
|
|
|
|
|
|
<!-- EOI Documents Section -->
|
|
|
|
|
<div v-if="hasEOIDocuments" class="mb-4">
|
|
|
|
|
<div class="text-subtitle-1 mb-2">EOI Documents</div>
|
|
|
|
|
<div class="d-flex flex-wrap ga-2">
|
|
|
|
|
<v-chip
|
|
|
|
|
v-for="(doc, index) in eoiDocuments"
|
|
|
|
|
:key="index"
|
|
|
|
|
color="primary"
|
|
|
|
|
variant="tonal"
|
|
|
|
|
prepend-icon="mdi-file-pdf-box"
|
|
|
|
|
:href="doc.url"
|
|
|
|
|
target="_blank"
|
|
|
|
|
component="a"
|
|
|
|
|
clickable
|
|
|
|
|
closable
|
|
|
|
|
@click:close="removeDocument(index)"
|
|
|
|
|
>
|
|
|
|
|
{{ doc.title || `EOI Document ${index + 1}` }}
|
|
|
|
|
</v-chip>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Generate EOI Button - Only show if no documents uploaded -->
|
2025-06-10 18:18:35 +02:00
|
|
|
<div v-if="!hasEOI && !hasEOIDocuments" class="d-flex flex-wrap gap-2">
|
2025-06-10 00:42:14 +02:00
|
|
|
<v-btn
|
2025-06-11 13:50:51 +02:00
|
|
|
@click="generateEOI(0, false)"
|
2025-06-10 00:42:14 +02:00
|
|
|
:loading="isGenerating"
|
|
|
|
|
color="primary"
|
|
|
|
|
variant="flat"
|
2025-06-10 16:48:40 +02:00
|
|
|
prepend-icon="mdi-file-document-plus"
|
2025-06-10 18:18:35 +02:00
|
|
|
:size="mobile ? 'default' : 'large'"
|
2025-06-10 00:42:14 +02:00
|
|
|
>
|
2025-06-10 16:48:40 +02:00
|
|
|
Generate EOI
|
2025-06-10 00:42:14 +02:00
|
|
|
</v-btn>
|
2025-06-10 18:18:35 +02:00
|
|
|
|
|
|
|
|
<v-btn
|
|
|
|
|
@click="showUploadDialog = true"
|
|
|
|
|
variant="outlined"
|
|
|
|
|
color="primary"
|
|
|
|
|
prepend-icon="mdi-upload"
|
|
|
|
|
:disabled="isUploading"
|
|
|
|
|
:size="mobile ? 'default' : 'large'"
|
|
|
|
|
>
|
|
|
|
|
Upload EOI Document
|
|
|
|
|
</v-btn>
|
2025-06-10 00:37:43 +02:00
|
|
|
</div>
|
2025-06-10 13:59:09 +02:00
|
|
|
|
2025-06-10 18:18:35 +02:00
|
|
|
<!-- Upload EOI Button - Only show if EOI exists or has documents -->
|
|
|
|
|
<div v-else-if="hasEOI || hasEOIDocuments" class="mb-4">
|
2025-06-10 13:59:09 +02:00
|
|
|
<v-btn
|
|
|
|
|
@click="showUploadDialog = true"
|
2025-06-10 18:18:35 +02:00
|
|
|
variant="tonal"
|
|
|
|
|
color="success"
|
|
|
|
|
prepend-icon="mdi-upload"
|
2025-06-10 13:59:09 +02:00
|
|
|
:disabled="isUploading"
|
2025-06-10 16:01:21 +02:00
|
|
|
:size="mobile ? 'default' : 'large'"
|
2025-06-10 13:59:09 +02:00
|
|
|
>
|
2025-06-10 18:18:35 +02:00
|
|
|
Upload Signed EOI
|
2025-06-10 13:59:09 +02:00
|
|
|
</v-btn>
|
|
|
|
|
</div>
|
2025-06-10 00:37:43 +02:00
|
|
|
|
2025-06-10 00:42:14 +02:00
|
|
|
<!-- EOI Status Badge -->
|
2025-06-10 16:01:21 +02:00
|
|
|
<div v-if="hasEOI || hasEOIDocuments" class="mb-4 d-flex align-center flex-wrap">
|
2025-06-10 00:42:14 +02:00
|
|
|
<v-chip
|
|
|
|
|
:color="getStatusColor(interest['EOI Status'])"
|
|
|
|
|
variant="tonal"
|
2025-06-10 16:01:21 +02:00
|
|
|
:prepend-icon="!mobile ? 'mdi-file-document-check' : undefined"
|
|
|
|
|
:size="mobile ? 'small' : 'default'"
|
|
|
|
|
class="mb-1"
|
2025-06-10 00:42:14 +02:00
|
|
|
>
|
|
|
|
|
{{ interest['EOI Status'] }}
|
|
|
|
|
</v-chip>
|
2025-06-10 02:39:00 +02:00
|
|
|
<span v-if="interest['EOI Time Sent']" class="text-caption text-grey-darken-1 ml-3">
|
2025-06-10 16:01:21 +02:00
|
|
|
{{ mobile ? '' : 'Sent: ' }}{{ formatDate(interest['EOI Time Sent']) }}
|
2025-06-10 00:42:14 +02:00
|
|
|
</span>
|
2025-06-10 00:37:43 +02:00
|
|
|
</div>
|
|
|
|
|
|
2025-06-10 15:52:30 +02:00
|
|
|
<!-- Signature Links - Only show if EOI is generated but not uploaded/signed -->
|
2025-06-11 13:50:51 +02:00
|
|
|
<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>
|
2025-06-10 00:42:14 +02:00
|
|
|
|
2025-06-11 13:50:51 +02:00
|
|
|
<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>
|
2025-06-10 00:42:14 +02:00
|
|
|
|
2025-06-11 13:50:51 +02:00
|
|
|
<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>
|
2025-06-10 00:37:43 +02:00
|
|
|
|
2025-06-11 13:50:51 +02:00
|
|
|
<!-- 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">
|
2025-06-10 00:42:14 +02:00
|
|
|
<v-btn
|
2025-06-11 13:50:51 +02:00
|
|
|
@click="generateEOI(0, true)"
|
2025-06-10 00:42:14 +02:00
|
|
|
:loading="isGenerating"
|
|
|
|
|
variant="text"
|
|
|
|
|
size="small"
|
|
|
|
|
prepend-icon="mdi-refresh"
|
|
|
|
|
>
|
|
|
|
|
Regenerate EOI
|
|
|
|
|
</v-btn>
|
2025-06-11 13:50:51 +02:00
|
|
|
|
|
|
|
|
<v-btn
|
|
|
|
|
v-if="canDeleteGenerated"
|
|
|
|
|
@click="showDeleteGeneratedDialog = true"
|
|
|
|
|
color="error"
|
|
|
|
|
variant="outlined"
|
|
|
|
|
size="small"
|
|
|
|
|
prepend-icon="mdi-delete"
|
|
|
|
|
>
|
|
|
|
|
Delete Generated EOI
|
|
|
|
|
</v-btn>
|
2025-06-10 00:42:14 +02:00
|
|
|
</div>
|
2025-06-10 14:32:20 +02:00
|
|
|
|
2025-06-10 15:52:30 +02:00
|
|
|
<!-- Delete EOI Button - Only show if EOI is uploaded/signed -->
|
|
|
|
|
<div v-if="isEOISigned" class="mt-4">
|
|
|
|
|
<v-btn
|
|
|
|
|
@click="showDeleteConfirmDialog = true"
|
|
|
|
|
color="error"
|
|
|
|
|
variant="outlined"
|
|
|
|
|
size="small"
|
|
|
|
|
prepend-icon="mdi-delete"
|
|
|
|
|
>
|
|
|
|
|
Delete EOI
|
|
|
|
|
</v-btn>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-06-10 00:42:14 +02:00
|
|
|
</v-card-text>
|
2025-06-10 13:59:09 +02:00
|
|
|
|
|
|
|
|
<!-- Upload Dialog -->
|
2025-06-10 16:01:21 +02:00
|
|
|
<v-dialog
|
|
|
|
|
v-model="showUploadDialog"
|
|
|
|
|
:max-width="mobile ? '100%' : '600'"
|
|
|
|
|
:fullscreen="mobile"
|
|
|
|
|
:transition="mobile ? 'dialog-bottom-transition' : 'dialog-transition'"
|
|
|
|
|
>
|
2025-06-10 13:59:09 +02:00
|
|
|
<v-card>
|
2025-06-10 14:32:20 +02:00
|
|
|
<v-card-title class="d-flex align-center">
|
|
|
|
|
<v-icon class="mr-2" color="primary">mdi-upload</v-icon>
|
|
|
|
|
Upload EOI Document
|
|
|
|
|
<v-spacer />
|
|
|
|
|
<v-btn icon="mdi-close" variant="text" @click="closeUploadDialog"></v-btn>
|
|
|
|
|
</v-card-title>
|
|
|
|
|
|
|
|
|
|
<v-divider />
|
|
|
|
|
|
2025-06-10 16:01:21 +02:00
|
|
|
<v-card-text :class="mobile ? 'pa-4' : 'pa-6'">
|
|
|
|
|
<div class="text-center" :class="mobile ? 'mb-4' : 'mb-6'">
|
|
|
|
|
<v-icon :size="mobile ? 60 : 80" color="primary" class="mb-4">mdi-file-pdf-box</v-icon>
|
|
|
|
|
<div :class="mobile ? 'text-body-1 mb-2' : 'text-h6 mb-2'">Choose how to upload your EOI</div>
|
|
|
|
|
<div :class="mobile ? 'text-caption' : 'text-body-2'" class="text-grey">Upload a signed PDF document</div>
|
2025-06-10 14:32:20 +02:00
|
|
|
</div>
|
|
|
|
|
|
2025-06-10 14:52:39 +02:00
|
|
|
<v-file-input
|
|
|
|
|
v-model="selectedFile"
|
2025-06-10 16:01:21 +02:00
|
|
|
:label="mobile ? 'Select PDF file' : 'Drop your PDF here or click to browse'"
|
2025-06-10 14:52:39 +02:00
|
|
|
accept=".pdf"
|
|
|
|
|
prepend-icon=""
|
|
|
|
|
variant="outlined"
|
2025-06-10 16:01:21 +02:00
|
|
|
:density="mobile ? 'compact' : 'comfortable'"
|
2025-06-10 14:52:39 +02:00
|
|
|
:rules="[v => !!v || 'Please select a file']"
|
2025-06-10 16:01:21 +02:00
|
|
|
:show-size="!mobile"
|
2025-06-10 14:52:39 +02:00
|
|
|
class="mt-4"
|
|
|
|
|
>
|
|
|
|
|
<template v-slot:prepend-inner>
|
2025-06-10 16:01:21 +02:00
|
|
|
<v-icon color="primary" :size="mobile ? 'small' : 'default'">mdi-file-pdf-box</v-icon>
|
2025-06-10 14:52:39 +02:00
|
|
|
</template>
|
|
|
|
|
</v-file-input>
|
2025-06-10 13:59:09 +02:00
|
|
|
</v-card-text>
|
2025-06-10 14:32:20 +02:00
|
|
|
|
|
|
|
|
<v-divider />
|
|
|
|
|
|
|
|
|
|
<v-card-actions class="pa-4">
|
2025-06-10 13:59:09 +02:00
|
|
|
<v-spacer />
|
2025-06-10 16:01:21 +02:00
|
|
|
<v-btn @click="closeUploadDialog" variant="text" :size="mobile ? 'default' : 'large'">
|
2025-06-10 14:32:20 +02:00
|
|
|
Cancel
|
|
|
|
|
</v-btn>
|
2025-06-10 13:59:09 +02:00
|
|
|
<v-btn
|
|
|
|
|
color="primary"
|
|
|
|
|
variant="flat"
|
|
|
|
|
@click="handleUpload"
|
|
|
|
|
:loading="isUploading"
|
2025-06-10 14:52:39 +02:00
|
|
|
:disabled="!selectedFile"
|
2025-06-10 16:01:21 +02:00
|
|
|
:size="mobile ? 'default' : 'large'"
|
|
|
|
|
:prepend-icon="!mobile ? 'mdi-upload' : undefined"
|
2025-06-10 13:59:09 +02:00
|
|
|
>
|
2025-06-10 16:01:21 +02:00
|
|
|
Upload{{ mobile ? '' : ' EOI' }}
|
2025-06-10 13:59:09 +02:00
|
|
|
</v-btn>
|
|
|
|
|
</v-card-actions>
|
|
|
|
|
</v-card>
|
|
|
|
|
</v-dialog>
|
2025-06-10 15:52:30 +02:00
|
|
|
|
|
|
|
|
<!-- Delete Confirmation Dialog -->
|
2025-06-10 16:01:21 +02:00
|
|
|
<v-dialog
|
|
|
|
|
v-model="showDeleteConfirmDialog"
|
|
|
|
|
:max-width="mobile ? '100%' : '400'"
|
|
|
|
|
:transition="mobile ? 'dialog-bottom-transition' : 'dialog-transition'"
|
|
|
|
|
>
|
2025-06-10 15:52:30 +02:00
|
|
|
<v-card>
|
|
|
|
|
<v-card-title class="d-flex align-center">
|
|
|
|
|
<v-icon class="mr-2" color="error">mdi-alert</v-icon>
|
|
|
|
|
Confirm EOI Deletion
|
|
|
|
|
</v-card-title>
|
|
|
|
|
|
|
|
|
|
<v-card-text>
|
|
|
|
|
Are you sure you want to delete the EOI document? This will:
|
|
|
|
|
<ul class="mt-2">
|
|
|
|
|
<li>Remove the uploaded EOI document</li>
|
|
|
|
|
<li>Reset the Sales Process Level to "Specific Qualified Interest"</li>
|
|
|
|
|
<li>Reset the EOI Status to "Awaiting Further Details"</li>
|
|
|
|
|
<li>Allow a new EOI to be generated</li>
|
|
|
|
|
</ul>
|
|
|
|
|
<div class="mt-3 text-error">This action cannot be undone.</div>
|
|
|
|
|
</v-card-text>
|
|
|
|
|
|
|
|
|
|
<v-card-actions>
|
|
|
|
|
<v-spacer />
|
|
|
|
|
<v-btn
|
|
|
|
|
@click="showDeleteConfirmDialog = false"
|
|
|
|
|
variant="text"
|
|
|
|
|
>
|
|
|
|
|
Cancel
|
|
|
|
|
</v-btn>
|
|
|
|
|
<v-btn
|
|
|
|
|
color="error"
|
|
|
|
|
variant="flat"
|
|
|
|
|
@click="deleteEOI"
|
|
|
|
|
:loading="isDeleting"
|
|
|
|
|
>
|
|
|
|
|
Delete EOI
|
|
|
|
|
</v-btn>
|
|
|
|
|
</v-card-actions>
|
|
|
|
|
</v-card>
|
|
|
|
|
</v-dialog>
|
2025-06-10 16:01:21 +02:00
|
|
|
|
2025-06-11 13:50:51 +02:00
|
|
|
<!-- 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>
|
|
|
|
|
|
2025-06-10 00:37:43 +02:00
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import type { Interest } from '~/utils/types';
|
|
|
|
|
|
|
|
|
|
const props = defineProps<{
|
|
|
|
|
interest: Interest;
|
|
|
|
|
}>();
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits<{
|
|
|
|
|
'eoi-generated': [data: { signingLinks: Record<string, string> }];
|
|
|
|
|
'update': [];
|
|
|
|
|
}>();
|
|
|
|
|
|
2025-06-10 16:01:21 +02:00
|
|
|
const { mobile } = useDisplay();
|
2025-06-10 13:59:09 +02:00
|
|
|
const toast = useToast();
|
2025-06-10 00:37:43 +02:00
|
|
|
const isGenerating = ref(false);
|
2025-06-10 13:59:09 +02:00
|
|
|
const showUploadDialog = ref(false);
|
|
|
|
|
const isUploading = ref(false);
|
|
|
|
|
const selectedFile = ref<File | null>(null);
|
2025-06-10 15:52:30 +02:00
|
|
|
const showDeleteConfirmDialog = ref(false);
|
|
|
|
|
const isDeleting = ref(false);
|
2025-06-11 13:50:51 +02:00
|
|
|
const signatureStatus = ref<any>(null);
|
|
|
|
|
const isCheckingStatus = ref(false);
|
|
|
|
|
const showDeleteGeneratedDialog = ref(false);
|
|
|
|
|
const isDeletingGenerated = ref(false);
|
|
|
|
|
const deleteSliderValue = ref(0);
|
2025-06-10 00:37:43 +02:00
|
|
|
|
|
|
|
|
const hasEOI = computed(() => {
|
|
|
|
|
return !!(props.interest['Signature Link Client'] ||
|
|
|
|
|
props.interest['Signature Link CC'] ||
|
|
|
|
|
props.interest['Signature Link Developer']);
|
|
|
|
|
});
|
|
|
|
|
|
2025-06-10 13:59:09 +02:00
|
|
|
const eoiDocuments = computed(() => {
|
|
|
|
|
return props.interest['EOI Document'] || [];
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const hasEOIDocuments = computed(() => {
|
|
|
|
|
return eoiDocuments.value.length > 0;
|
|
|
|
|
});
|
|
|
|
|
|
2025-06-10 15:52:30 +02:00
|
|
|
const isEOISigned = computed(() => {
|
|
|
|
|
return props.interest['EOI Status'] === 'Signed';
|
|
|
|
|
});
|
|
|
|
|
|
2025-06-11 13:50:51 +02:00
|
|
|
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) => {
|
2025-06-10 00:37:43 +02:00
|
|
|
isGenerating.value = true;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const response = await $fetch<{
|
|
|
|
|
success: boolean;
|
|
|
|
|
documentId: string | number;
|
|
|
|
|
clientSigningUrl: string;
|
|
|
|
|
signingLinks: Record<string, string>;
|
|
|
|
|
}>('/api/email/generate-eoi-document', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
'x-tag': '094ut234'
|
|
|
|
|
},
|
|
|
|
|
body: {
|
2025-06-11 13:50:51 +02:00
|
|
|
interestId: props.interest.Id.toString(),
|
|
|
|
|
regenerate: regenerate
|
2025-06-10 00:37:43 +02:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (response.success) {
|
2025-06-10 13:59:09 +02:00
|
|
|
toast.success(response.documentId === 'existing'
|
2025-06-10 00:37:43 +02:00
|
|
|
? 'EOI already exists - signature links retrieved'
|
2025-06-11 13:50:51 +02:00
|
|
|
: regenerate
|
|
|
|
|
? 'EOI regenerated successfully'
|
|
|
|
|
: 'EOI generated successfully');
|
2025-06-10 00:37:43 +02:00
|
|
|
|
|
|
|
|
emit('eoi-generated', { signingLinks: response.signingLinks });
|
|
|
|
|
emit('update'); // Trigger parent to refresh data
|
2025-06-10 13:59:09 +02:00
|
|
|
} else {
|
|
|
|
|
throw new Error('EOI generation failed');
|
2025-06-10 00:37:43 +02:00
|
|
|
}
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
console.error('Failed to generate EOI:', error);
|
2025-06-10 13:59:09 +02:00
|
|
|
|
|
|
|
|
// Retry logic
|
|
|
|
|
if (retryCount < 3) {
|
|
|
|
|
console.log(`Retrying EOI generation... Attempt ${retryCount + 2}/4`);
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, (retryCount + 1) * 1000));
|
2025-06-11 13:50:51 +02:00
|
|
|
return generateEOI(retryCount + 1, regenerate);
|
2025-06-10 13:59:09 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Show error message after all retries failed
|
|
|
|
|
toast.error(error.data?.statusMessage || error.message || 'Failed to generate EOI after multiple attempts');
|
2025-06-10 00:37:43 +02:00
|
|
|
} finally {
|
|
|
|
|
isGenerating.value = false;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-06-10 00:53:49 +02:00
|
|
|
const copyLink = async (link: string | undefined) => {
|
2025-06-10 00:37:43 +02:00
|
|
|
if (!link) return;
|
|
|
|
|
|
2025-06-10 00:53:49 +02:00
|
|
|
try {
|
|
|
|
|
await navigator.clipboard.writeText(link);
|
2025-06-10 13:59:09 +02:00
|
|
|
toast.success('Signature link copied to clipboard');
|
2025-06-10 00:53:49 +02:00
|
|
|
|
|
|
|
|
// Update EOI Time Sent if not already set
|
|
|
|
|
if (!props.interest['EOI Time Sent']) {
|
|
|
|
|
try {
|
|
|
|
|
await $fetch('/api/update-interest', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
'x-tag': '094ut234'
|
|
|
|
|
},
|
|
|
|
|
body: {
|
|
|
|
|
id: props.interest.Id.toString(),
|
|
|
|
|
data: {
|
|
|
|
|
'EOI Time Sent': new Date().toISOString()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
emit('update'); // Refresh parent data
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to update EOI Time Sent:', error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
2025-06-10 13:59:09 +02:00
|
|
|
toast.error('Failed to copy link');
|
2025-06-10 00:53:49 +02:00
|
|
|
}
|
2025-06-10 00:37:43 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const formatDate = (dateString: string) => {
|
2025-06-10 01:19:14 +02:00
|
|
|
if (!dateString) return '';
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const date = new Date(dateString);
|
|
|
|
|
// Check if date is valid
|
|
|
|
|
if (isNaN(date.getTime())) {
|
|
|
|
|
return dateString; // Return original if parsing fails
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return date.toLocaleString('en-GB', {
|
|
|
|
|
day: '2-digit',
|
|
|
|
|
month: '2-digit',
|
|
|
|
|
year: 'numeric',
|
|
|
|
|
hour: '2-digit',
|
|
|
|
|
minute: '2-digit',
|
|
|
|
|
hour12: false
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Date formatting error:', error);
|
|
|
|
|
return dateString;
|
|
|
|
|
}
|
2025-06-10 00:37:43 +02:00
|
|
|
};
|
2025-06-10 00:42:14 +02:00
|
|
|
|
|
|
|
|
const getStatusColor = (status: string) => {
|
|
|
|
|
switch (status) {
|
|
|
|
|
case 'Waiting for Signatures':
|
|
|
|
|
return 'warning';
|
|
|
|
|
case 'Signed':
|
|
|
|
|
return 'success';
|
|
|
|
|
default:
|
|
|
|
|
return 'grey';
|
|
|
|
|
}
|
|
|
|
|
};
|
2025-06-10 13:59:09 +02:00
|
|
|
|
|
|
|
|
const removeDocument = async (index: number) => {
|
|
|
|
|
// For now, we'll just show a message since removing documents
|
|
|
|
|
// would require updating the database
|
|
|
|
|
toast.warning('Document removal is not yet implemented');
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const uploadEOI = async (file: File) => {
|
|
|
|
|
isUploading.value = true;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const formData = new FormData();
|
|
|
|
|
formData.append('file', file);
|
|
|
|
|
|
|
|
|
|
const response = await $fetch<{ success: boolean; document: any; message: string }>('/api/eoi/upload-document', {
|
|
|
|
|
method: 'POST',
|
2025-06-10 15:42:00 +02:00
|
|
|
headers: {
|
|
|
|
|
'x-tag': '094ut234'
|
|
|
|
|
},
|
2025-06-10 13:59:09 +02:00
|
|
|
query: {
|
|
|
|
|
interestId: props.interest.Id.toString()
|
|
|
|
|
},
|
|
|
|
|
body: formData
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (response.success) {
|
|
|
|
|
toast.success('EOI document uploaded successfully');
|
|
|
|
|
showUploadDialog.value = false;
|
|
|
|
|
selectedFile.value = null; // Reset file selection
|
|
|
|
|
emit('update'); // Refresh parent data
|
|
|
|
|
}
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
console.error('Failed to upload EOI:', error);
|
|
|
|
|
toast.error(error.data?.statusMessage || 'Failed to upload EOI document');
|
|
|
|
|
} finally {
|
|
|
|
|
isUploading.value = false;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleUpload = () => {
|
|
|
|
|
if (selectedFile.value) {
|
|
|
|
|
uploadEOI(selectedFile.value);
|
|
|
|
|
}
|
|
|
|
|
};
|
2025-06-10 14:32:20 +02:00
|
|
|
|
|
|
|
|
const closeUploadDialog = () => {
|
|
|
|
|
showUploadDialog.value = false;
|
|
|
|
|
selectedFile.value = null;
|
|
|
|
|
};
|
2025-06-10 15:52:30 +02:00
|
|
|
|
|
|
|
|
const deleteEOI = async () => {
|
|
|
|
|
isDeleting.value = true;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const response = await $fetch<{ success: boolean; message: string }>('/api/eoi/delete-document', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
'x-tag': '094ut234'
|
|
|
|
|
},
|
|
|
|
|
body: {
|
|
|
|
|
interestId: props.interest.Id.toString()
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (response.success) {
|
|
|
|
|
toast.success('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');
|
|
|
|
|
} finally {
|
|
|
|
|
isDeleting.value = false;
|
|
|
|
|
}
|
|
|
|
|
};
|
2025-06-11 13:50:51 +02:00
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-06-10 00:37:43 +02:00
|
|
|
</script>
|