port-nimara-client-portal/components/EOISection.vue

585 lines
18 KiB
Vue
Raw Normal View History

<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">
<!-- 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 -->
<div v-if="!hasEOI && !hasEOIDocuments" class="mb-4">
2025-06-10 00:42:14 +02:00
<v-btn
@click="generateEOI"
:loading="isGenerating"
color="primary"
variant="flat"
2025-06-10 16:01:21 +02:00
:prepend-icon="!mobile ? 'mdi-file-document-plus' : undefined"
:icon="mobile ? 'mdi-file-document-plus' : undefined"
:size="mobile ? 'default' : 'default'"
2025-06-10 00:42:14 +02:00
>
2025-06-10 16:01:21 +02:00
<span v-if="!mobile">Generate EOI</span>
2025-06-10 00:42:14 +02:00
</v-btn>
</div>
<!-- Upload EOI Button -->
<div class="mb-4">
<v-btn
@click="showUploadDialog = true"
2025-06-10 14:32:20 +02:00
:variant="hasEOI ? 'tonal' : 'outlined'"
:color="hasEOI ? 'success' : 'primary'"
2025-06-10 16:01:21 +02:00
:prepend-icon="!mobile ? 'mdi-upload' : undefined"
:disabled="isUploading"
2025-06-10 16:01:21 +02:00
:size="mobile ? 'default' : 'large'"
>
2025-06-10 16:01:21 +02:00
{{ mobile ? 'Upload EOI' : (hasEOI ? 'Upload Signed EOI' : 'Upload EOI Document') }}
</v-btn>
</div>
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>
<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>
</div>
2025-06-10 15:52:30 +02:00
<!-- Signature Links - Only show if EOI is generated but not uploaded/signed -->
2025-06-10 16:01:21 +02:00
<v-list v-if="hasEOI && !isEOISigned" :density="mobile ? 'compact' : 'comfortable'">
2025-06-10 00:42:14 +02:00
<v-list-item class="mb-2">
<template v-slot:prepend>
2025-06-10 16:01:21 +02:00
<v-avatar color="primary" :size="mobile ? 32 : 40">
<v-icon :size="mobile ? 'small' : 'default'">mdi-account</v-icon>
2025-06-10 00:42:14 +02:00
</v-avatar>
</template>
2025-06-10 16:01:21 +02:00
<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>
2025-06-10 00:42:14 +02:00
<template v-slot:append>
<v-btn
icon="mdi-content-copy"
variant="text"
2025-06-10 16:01:21 +02:00
:size="mobile ? 'small' : 'default'"
2025-06-10 00:42:14 +02:00
@click="copyLink(interest['Signature Link Client'])"
></v-btn>
</template>
</v-list-item>
<v-list-item class="mb-2">
<template v-slot:prepend>
2025-06-10 16:01:21 +02:00
<v-avatar color="success" :size="mobile ? 32 : 40">
<v-icon :size="mobile ? 'small' : 'default'">mdi-account-check</v-icon>
2025-06-10 00:42:14 +02:00
</v-avatar>
</template>
2025-06-10 16:01:21 +02:00
<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>
2025-06-10 00:42:14 +02:00
<template v-slot:append>
<v-btn
icon="mdi-content-copy"
variant="text"
2025-06-10 16:01:21 +02:00
:size="mobile ? 'small' : 'default'"
2025-06-10 00:42:14 +02:00
@click="copyLink(interest['Signature Link CC'])"
></v-btn>
</template>
</v-list-item>
<v-list-item class="mb-2">
<template v-slot:prepend>
2025-06-10 16:01:21 +02:00
<v-avatar color="secondary" :size="mobile ? 32 : 40">
<v-icon :size="mobile ? 'small' : 'default'">mdi-account-tie</v-icon>
2025-06-10 00:42:14 +02:00
</v-avatar>
</template>
2025-06-10 16:01:21 +02:00
<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>
2025-06-10 00:42:14 +02:00
<template v-slot:append>
<v-btn
icon="mdi-content-copy"
variant="text"
2025-06-10 16:01:21 +02:00
:size="mobile ? 'small' : 'default'"
2025-06-10 00:42:14 +02:00
@click="copyLink(interest['Signature Link Developer'])"
></v-btn>
</template>
</v-list-item>
</v-list>
2025-06-10 15:52:30 +02:00
<!-- Regenerate Button - Only show if EOI is generated but not uploaded/signed -->
<div v-if="hasEOI && !isEOISigned" class="mt-4">
2025-06-10 00:42:14 +02:00
<v-btn
@click="generateEOI"
:loading="isGenerating"
variant="text"
size="small"
prepend-icon="mdi-refresh"
>
Regenerate EOI
</v-btn>
</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>
<!-- Reminder Settings - Only show if EOI is generated but not uploaded/signed -->
<v-divider v-if="hasEOI && !isEOISigned" class="mt-4 mb-4" />
<v-row v-if="hasEOI && !isEOISigned" dense align="center">
2025-06-10 16:01:21 +02:00
<v-col :cols="mobile ? 12 : 'auto'">
2025-06-10 14:32:20 +02:00
<v-switch
v-model="remindersEnabled"
@update:model-value="updateReminderSettings"
color="primary"
hide-details
density="compact"
>
<template v-slot:label>
2025-06-10 16:01:21 +02:00
<span :class="mobile ? 'text-caption' : 'text-body-2'">
2025-06-10 14:32:20 +02:00
Automatic Reminders
2025-06-10 16:01:21 +02:00
<v-icon
v-if="mobile"
size="x-small"
class="ml-1"
@click.stop="showReminderInfo = true"
>
mdi-information-outline
</v-icon>
<v-tooltip v-else activator="parent" location="top">
2025-06-10 14:32:20 +02:00
<div style="max-width: 300px">
When enabled, automatic reminders will be sent at 9am and 4pm daily to unsigned recipients
</div>
</v-tooltip>
</span>
</template>
</v-switch>
</v-col>
</v-row>
2025-06-10 00:42:14 +02:00
</v-card-text>
<!-- 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'"
>
<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>
</v-card-text>
2025-06-10 14:32:20 +02:00
<v-divider />
<v-card-actions class="pa-4">
<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>
<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 16:01:21 +02:00
Upload{{ mobile ? '' : ' EOI' }}
</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
<!-- Mobile Reminder Info Dialog -->
<v-dialog v-if="mobile" v-model="showReminderInfo" max-width="300">
<v-card>
<v-card-title class="text-h6">
Automatic Reminders
</v-card-title>
<v-card-text>
When enabled, automatic reminders will be sent at 9am and 4pm daily to unsigned recipients.
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn color="primary" variant="text" @click="showReminderInfo = false">
Got it
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</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();
const toast = useToast();
const isGenerating = ref(false);
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-10 16:01:21 +02:00
const showReminderInfo = ref(false);
2025-06-10 14:32:20 +02:00
// Reminder settings
const remindersEnabled = ref(true);
// Initialize reminder state from interest data
watch(() => props.interest, (newInterest) => {
if (newInterest) {
remindersEnabled.value = (newInterest as any)['reminder_enabled'] !== false;
}
}, { immediate: true });
const hasEOI = computed(() => {
return !!(props.interest['Signature Link Client'] ||
props.interest['Signature Link CC'] ||
props.interest['Signature Link Developer']);
});
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';
});
const generateEOI = async (retryCount = 0) => {
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: {
interestId: props.interest.Id.toString()
}
});
if (response.success) {
toast.success(response.documentId === 'existing'
? 'EOI already exists - signature links retrieved'
: 'EOI generated successfully');
emit('eoi-generated', { signingLinks: response.signingLinks });
emit('update'); // Trigger parent to refresh data
} else {
throw new Error('EOI generation failed');
}
} catch (error: any) {
console.error('Failed to generate EOI:', error);
// Retry logic
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);
}
// Show error message after all retries failed
toast.error(error.data?.statusMessage || error.message || 'Failed to generate EOI after multiple attempts');
} finally {
isGenerating.value = false;
}
};
2025-06-10 00:53:49 +02:00
const copyLink = async (link: string | undefined) => {
if (!link) return;
2025-06-10 00:53:49 +02:00
try {
await navigator.clipboard.writeText(link);
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) {
toast.error('Failed to copy link');
2025-06-10 00:53:49 +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:42:14 +02:00
const getStatusColor = (status: string) => {
switch (status) {
case 'Waiting for Signatures':
return 'warning';
case 'Signed':
return 'success';
default:
return 'grey';
}
};
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'
},
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 updateReminderSettings = async (value: boolean | null) => {
if (value === null) return;
try {
await $fetch('/api/update-interest', {
method: 'POST',
headers: {
'x-tag': '094ut234'
},
body: {
id: props.interest.Id.toString(),
data: {
'reminder_enabled': value
}
}
});
toast.success(`Automatic reminders ${value ? 'enabled' : 'disabled'}`);
emit('update'); // Refresh parent data
} catch (error) {
console.error('Failed to update reminder settings:', error);
toast.error('Failed to update reminder settings');
// Revert the switch on error
remindersEnabled.value = !value;
}
};
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;
}
};
</script>