Refactor EOI management into dedicated component
Extract EOI links and generation functionality from InterestDetailsModal into a new reusable EOISection component. This improves code organization and maintainability while adding debounce support for form submissions. - Create new EOISection.vue component for EOI management - Remove inline EOI links section from InterestDetailsModal - Add debounce utility for form submission handling - Update email generation and thread fetching logic - Update related types and utilities
This commit is contained in:
parent
76d04a1e2a
commit
d9fb94a76c
|
|
@ -0,0 +1,177 @@
|
||||||
|
<template>
|
||||||
|
<div class="border rounded-lg p-4 bg-gray-50">
|
||||||
|
<h3 class="text-lg font-semibold mb-4">EOI Management</h3>
|
||||||
|
|
||||||
|
<!-- Generate EOI Button -->
|
||||||
|
<div v-if="!hasEOI" class="mb-4">
|
||||||
|
<button
|
||||||
|
@click="generateEOI"
|
||||||
|
:disabled="isGenerating"
|
||||||
|
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{{ isGenerating ? 'Generating EOI...' : 'Generate EOI' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- EOI Status Badge -->
|
||||||
|
<div v-if="hasEOI" class="mb-4">
|
||||||
|
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium"
|
||||||
|
:class="{
|
||||||
|
'bg-yellow-100 text-yellow-800': interest['EOI Status'] === 'Waiting for Signatures',
|
||||||
|
'bg-green-100 text-green-800': interest['EOI Status'] === 'Signed',
|
||||||
|
'bg-gray-100 text-gray-800': interest['EOI Status'] === 'Awaiting Further Details'
|
||||||
|
}">
|
||||||
|
{{ interest['EOI Status'] }}
|
||||||
|
</span>
|
||||||
|
<span v-if="interest['EOI Time Sent']" class="ml-2 text-sm text-gray-600">
|
||||||
|
Sent: {{ formatDate(interest['EOI Time Sent']) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Signature Links -->
|
||||||
|
<div v-if="hasEOI" class="space-y-3">
|
||||||
|
<div class="border rounded p-3 bg-white">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<span class="font-medium">Client Signature Link</span>
|
||||||
|
<span class="text-sm text-gray-500 ml-2">({{ interest['Full Name'] }})</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
@click="copyLink(interest['Signature Link Client'])"
|
||||||
|
class="text-blue-600 hover:text-blue-800 text-sm flex items-center"
|
||||||
|
>
|
||||||
|
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||||
|
</svg>
|
||||||
|
Copy
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border rounded p-3 bg-white">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<span class="font-medium">CC Signature Link</span>
|
||||||
|
<span class="text-sm text-gray-500 ml-2">(Oscar Faragher)</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
@click="copyLink(interest['Signature Link CC'])"
|
||||||
|
class="text-blue-600 hover:text-blue-800 text-sm flex items-center"
|
||||||
|
>
|
||||||
|
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||||
|
</svg>
|
||||||
|
Copy
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border rounded p-3 bg-white">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<span class="font-medium">Developer Signature Link</span>
|
||||||
|
<span class="text-sm text-gray-500 ml-2">(David Mizrahi)</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
@click="copyLink(interest['Signature Link Developer'])"
|
||||||
|
class="text-blue-600 hover:text-blue-800 text-sm flex items-center"
|
||||||
|
>
|
||||||
|
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||||
|
</svg>
|
||||||
|
Copy
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Regenerate Button -->
|
||||||
|
<div v-if="hasEOI && interest['EOI Status'] !== 'Signed'" class="mt-4">
|
||||||
|
<button
|
||||||
|
@click="generateEOI"
|
||||||
|
:disabled="isGenerating"
|
||||||
|
class="text-sm text-gray-600 hover:text-gray-800 underline"
|
||||||
|
>
|
||||||
|
Regenerate EOI
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</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': [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { showToast } = useToast();
|
||||||
|
const isGenerating = ref(false);
|
||||||
|
|
||||||
|
const hasEOI = computed(() => {
|
||||||
|
return !!(props.interest['Signature Link Client'] ||
|
||||||
|
props.interest['Signature Link CC'] ||
|
||||||
|
props.interest['Signature Link Developer']);
|
||||||
|
});
|
||||||
|
|
||||||
|
const generateEOI = async () => {
|
||||||
|
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) {
|
||||||
|
showToast(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
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to generate EOI:', error);
|
||||||
|
showToast(error.data?.statusMessage || 'Failed to generate EOI');
|
||||||
|
} finally {
|
||||||
|
isGenerating.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyLink = (link: string | undefined) => {
|
||||||
|
if (!link) return;
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(link).then(() => {
|
||||||
|
showToast('Signature link copied to clipboard');
|
||||||
|
}).catch(() => {
|
||||||
|
showToast('Failed to copy link');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateString: string) => {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -244,7 +244,7 @@
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
<v-form @submit.prevent="saveInterest">
|
<v-form @submit.prevent="handleFormSubmit">
|
||||||
<!-- Contact Information Section -->
|
<!-- Contact Information Section -->
|
||||||
<v-card variant="flat" class="mb-6">
|
<v-card variant="flat" class="mb-6">
|
||||||
<v-card-title class="text-h6 d-flex align-center pb-4">
|
<v-card-title class="text-h6 d-flex align-center pb-4">
|
||||||
|
|
@ -626,79 +626,14 @@
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
<!-- EOI Links Section (only shows if EOI has been sent) -->
|
<!-- EOI Management Section -->
|
||||||
<v-card
|
<v-card variant="flat" class="mb-6">
|
||||||
v-if="hasEOILinks"
|
<EOISection
|
||||||
variant="flat"
|
v-if="interest"
|
||||||
class="mb-6"
|
:interest="interest"
|
||||||
>
|
@eoi-generated="onEOIGenerated"
|
||||||
<v-card-title class="text-h6 d-flex align-center pb-4">
|
@update="onInterestUpdated"
|
||||||
<v-icon class="mr-2" color="primary">mdi-link-variant</v-icon>
|
/>
|
||||||
EOI Links
|
|
||||||
</v-card-title>
|
|
||||||
<v-card-text class="pt-2">
|
|
||||||
<v-list>
|
|
||||||
<v-list-item
|
|
||||||
v-if="(interest as any)['EOI Client Link']"
|
|
||||||
class="mb-2"
|
|
||||||
>
|
|
||||||
<template v-slot:prepend>
|
|
||||||
<v-avatar color="primary" size="40">
|
|
||||||
<v-icon>mdi-account</v-icon>
|
|
||||||
</v-avatar>
|
|
||||||
</template>
|
|
||||||
<v-list-item-title>Client ({{ interest['Full Name'] }})</v-list-item-title>
|
|
||||||
<v-list-item-subtitle class="text-truncate">{{ (interest as any)['EOI Client Link'] }}</v-list-item-subtitle>
|
|
||||||
<template v-slot:append>
|
|
||||||
<v-btn
|
|
||||||
icon="mdi-content-copy"
|
|
||||||
variant="text"
|
|
||||||
@click="copyToClipboard((interest as any)['EOI Client Link'], 'Client')"
|
|
||||||
></v-btn>
|
|
||||||
</template>
|
|
||||||
</v-list-item>
|
|
||||||
|
|
||||||
<v-list-item
|
|
||||||
v-if="(interest as any)['EOI Oscar Link']"
|
|
||||||
class="mb-2"
|
|
||||||
>
|
|
||||||
<template v-slot:prepend>
|
|
||||||
<v-avatar color="success" size="40">
|
|
||||||
<v-icon>mdi-account-check</v-icon>
|
|
||||||
</v-avatar>
|
|
||||||
</template>
|
|
||||||
<v-list-item-title>Oscar Faragher (Approver)</v-list-item-title>
|
|
||||||
<v-list-item-subtitle class="text-truncate">{{ (interest as any)['EOI Oscar Link'] }}</v-list-item-subtitle>
|
|
||||||
<template v-slot:append>
|
|
||||||
<v-btn
|
|
||||||
icon="mdi-content-copy"
|
|
||||||
variant="text"
|
|
||||||
@click="copyToClipboard((interest as any)['EOI Oscar Link'], 'Oscar Faragher')"
|
|
||||||
></v-btn>
|
|
||||||
</template>
|
|
||||||
</v-list-item>
|
|
||||||
|
|
||||||
<v-list-item
|
|
||||||
v-if="(interest as any)['EOI David Link']"
|
|
||||||
class="mb-2"
|
|
||||||
>
|
|
||||||
<template v-slot:prepend>
|
|
||||||
<v-avatar color="secondary" size="40">
|
|
||||||
<v-icon>mdi-account-tie</v-icon>
|
|
||||||
</v-avatar>
|
|
||||||
</template>
|
|
||||||
<v-list-item-title>David Mizrahi (Signer)</v-list-item-title>
|
|
||||||
<v-list-item-subtitle class="text-truncate">{{ (interest as any)['EOI David Link'] }}</v-list-item-subtitle>
|
|
||||||
<template v-slot:append>
|
|
||||||
<v-btn
|
|
||||||
icon="mdi-content-copy"
|
|
||||||
variant="text"
|
|
||||||
@click="copyToClipboard((interest as any)['EOI David Link'], 'David Mizrahi')"
|
|
||||||
></v-btn>
|
|
||||||
</template>
|
|
||||||
</v-list-item>
|
|
||||||
</v-list>
|
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
<!-- Email Communication Section -->
|
<!-- Email Communication Section -->
|
||||||
|
|
@ -714,10 +649,30 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, computed, watch, onMounted } from "vue";
|
import { ref, computed, watch, onMounted, onUnmounted } from "vue";
|
||||||
import type { Interest, Berth } from "@/utils/types";
|
import type { Interest, Berth } from "@/utils/types";
|
||||||
|
|
||||||
|
// Simple debounce implementation
|
||||||
|
function debounce<T extends (...args: any[]) => any>(
|
||||||
|
func: T,
|
||||||
|
wait: number
|
||||||
|
): ((...args: Parameters<T>) => void) & { cancel: () => void } {
|
||||||
|
let timeout: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
|
const debounced = (...args: Parameters<T>) => {
|
||||||
|
if (timeout) clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(() => func(...args), wait);
|
||||||
|
};
|
||||||
|
|
||||||
|
debounced.cancel = () => {
|
||||||
|
if (timeout) clearTimeout(timeout);
|
||||||
|
};
|
||||||
|
|
||||||
|
return debounced;
|
||||||
|
}
|
||||||
import PhoneInput from "./PhoneInput.vue";
|
import PhoneInput from "./PhoneInput.vue";
|
||||||
import EmailCommunication from "./EmailCommunication.vue";
|
import EmailCommunication from "./EmailCommunication.vue";
|
||||||
|
import EOISection from "./EOISection.vue";
|
||||||
import {
|
import {
|
||||||
InterestSalesProcessLevelFlow,
|
InterestSalesProcessLevelFlow,
|
||||||
InterestLeadCategoryFlow,
|
InterestLeadCategoryFlow,
|
||||||
|
|
@ -753,6 +708,10 @@ const toast = useToast();
|
||||||
// Local copy of the interest for editing
|
// Local copy of the interest for editing
|
||||||
const interest = ref<Interest | null>(null);
|
const interest = ref<Interest | null>(null);
|
||||||
|
|
||||||
|
// Auto-save related
|
||||||
|
const hasUnsavedChanges = ref(false);
|
||||||
|
const autoSaveTimer = ref<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
// Loading states for buttons
|
// Loading states for buttons
|
||||||
const isSaving = ref(false);
|
const isSaving = ref(false);
|
||||||
const isRequestingMoreInfo = ref(false);
|
const isRequestingMoreInfo = ref(false);
|
||||||
|
|
@ -768,11 +727,32 @@ const selectedBerthRecommendations = ref<number[]>([]);
|
||||||
const originalBerths = ref<number[]>([]);
|
const originalBerths = ref<number[]>([]);
|
||||||
const originalBerthRecommendations = ref<number[]>([]);
|
const originalBerthRecommendations = ref<number[]>([]);
|
||||||
|
|
||||||
|
// Auto-save function (debounced)
|
||||||
|
const autoSave = debounce(async () => {
|
||||||
|
if (!hasUnsavedChanges.value || !interest.value) return;
|
||||||
|
|
||||||
|
console.log('Auto-saving interest...');
|
||||||
|
await saveInterest(true); // Pass true to indicate auto-save
|
||||||
|
}, 2000); // 2 second delay
|
||||||
|
|
||||||
|
// Watch for changes to trigger auto-save
|
||||||
|
watch(
|
||||||
|
() => interest.value,
|
||||||
|
(newValue, oldValue) => {
|
||||||
|
if (newValue && oldValue && JSON.stringify(newValue) !== JSON.stringify(oldValue)) {
|
||||||
|
hasUnsavedChanges.value = true;
|
||||||
|
autoSave();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
// Sync the local copy with the prop
|
// Sync the local copy with the prop
|
||||||
watch(
|
watch(
|
||||||
() => props.selectedInterest,
|
() => props.selectedInterest,
|
||||||
async (newInterest) => {
|
async (newInterest) => {
|
||||||
if (newInterest) {
|
if (newInterest) {
|
||||||
|
hasUnsavedChanges.value = false;
|
||||||
interest.value = { ...newInterest };
|
interest.value = { ...newInterest };
|
||||||
// Load linked berths and recommendations
|
// Load linked berths and recommendations
|
||||||
await loadLinkedBerths();
|
await loadLinkedBerths();
|
||||||
|
|
@ -812,7 +792,11 @@ const closeModal = () => {
|
||||||
isOpen.value = false;
|
isOpen.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveInterest = async () => {
|
const handleFormSubmit = () => {
|
||||||
|
saveInterest();
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveInterest = async (isAutoSave = false) => {
|
||||||
if (interest.value) {
|
if (interest.value) {
|
||||||
isSaving.value = true;
|
isSaving.value = true;
|
||||||
try {
|
try {
|
||||||
|
|
@ -833,12 +817,21 @@ const saveInterest = async () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
toast.success("Interest saved successfully!");
|
hasUnsavedChanges.value = false;
|
||||||
emit("save", interest.value);
|
|
||||||
closeModal();
|
if (!isAutoSave) {
|
||||||
|
toast.success("Interest saved successfully!");
|
||||||
|
emit("save", interest.value);
|
||||||
|
closeModal();
|
||||||
|
} else {
|
||||||
|
// For auto-save, just emit save to refresh parent
|
||||||
|
emit("save", interest.value);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to save interest:", error);
|
console.error("Failed to save interest:", error);
|
||||||
toast.error("Failed to save interest. Please try again.");
|
if (!isAutoSave) {
|
||||||
|
toast.error("Failed to save interest. Please try again.");
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
isSaving.value = false;
|
isSaving.value = false;
|
||||||
}
|
}
|
||||||
|
|
@ -1188,6 +1181,12 @@ const copyToClipboard = async (text: string, recipient: string) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle EOI generated event
|
||||||
|
const onEOIGenerated = (data: { signingLinks: Record<string, string> }) => {
|
||||||
|
console.log('EOI generated with links:', data.signingLinks);
|
||||||
|
// The EOISection component will trigger the update event, so we just need to handle that
|
||||||
|
};
|
||||||
|
|
||||||
// Handle interest updated event from EmailCommunication
|
// Handle interest updated event from EmailCommunication
|
||||||
const onInterestUpdated = async () => {
|
const onInterestUpdated = async () => {
|
||||||
// Reload the interest data
|
// Reload the interest data
|
||||||
|
|
@ -1216,4 +1215,13 @@ const onInterestUpdated = async () => {
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadAvailableBerths();
|
loadAvailableBerths();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Cleanup on unmount
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (autoSaveTimer.value) {
|
||||||
|
clearTimeout(autoSaveTimer.value);
|
||||||
|
}
|
||||||
|
// Cancel any pending auto-save
|
||||||
|
autoSave.cancel();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,37 @@ NUXT_DOCUMENSO_API_KEY=your-actual-api-key
|
||||||
NUXT_DOCUMENSO_BASE_URL=https://signatures.portnimara.dev
|
NUXT_DOCUMENSO_BASE_URL=https://signatures.portnimara.dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 12. EOI Management System
|
||||||
|
- **Added EOI Section Component**: New component to manage EOI document generation and signature links
|
||||||
|
- **Features Implemented**:
|
||||||
|
- Generate EOI button that creates documents via Documenso API
|
||||||
|
- Checks for existing EOI before generating new one (prevents duplicates)
|
||||||
|
- Displays all 3 signature links (Client, CC, Developer) with copy-to-clipboard functionality
|
||||||
|
- Shows EOI status badge (Awaiting Further Details, Waiting for Signatures, Signed)
|
||||||
|
- Regenerate option for non-signed documents
|
||||||
|
- **Auto-Updates on EOI Generation**:
|
||||||
|
- EOI Status → "Waiting for Signatures"
|
||||||
|
- Sales Process Level → "LOI and NDA Sent"
|
||||||
|
- EOI Time Sent → Current timestamp
|
||||||
|
- Extra Comments → Appends "EOI Sent [timestamp]"
|
||||||
|
- **Database Storage**: Links stored in new columns:
|
||||||
|
- `Signature Link Client`
|
||||||
|
- `Signature Link CC`
|
||||||
|
- `Signature Link Developer`
|
||||||
|
|
||||||
|
### 13. Auto-Save Functionality
|
||||||
|
- **Implemented**: Interest details now auto-save after 2 seconds of inactivity
|
||||||
|
- **Features**:
|
||||||
|
- Debounced save to prevent excessive API calls
|
||||||
|
- Silent save without success notifications
|
||||||
|
- Automatically triggers parent refresh to keep data in sync
|
||||||
|
- Cancels pending saves on component unmount
|
||||||
|
|
||||||
|
### 14. IMAP BCC Search Fix
|
||||||
|
- **Problem**: IMAP search was failing with "Cannot read properties of null"
|
||||||
|
- **Cause**: BCC search criteria not supported by all IMAP servers
|
||||||
|
- **Solution**: Removed BCC from search criteria, now only searches FROM, TO, and CC fields
|
||||||
|
|
||||||
## How It Works Now
|
## How It Works Now
|
||||||
|
|
||||||
1. **Email Session Management**:
|
1. **Email Session Management**:
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,11 @@
|
||||||
"": {
|
"": {
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@vite-pwa/nuxt": "^0.10.6",
|
"@vite-pwa/nuxt": "^0.10.6",
|
||||||
"formidable": "^3.5.4",
|
"formidable": "^3.5.4",
|
||||||
"imap": "^0.8.19",
|
"imap": "^0.8.19",
|
||||||
|
"lodash-es": "^4.17.21",
|
||||||
"mailparser": "^3.7.3",
|
"mailparser": "^3.7.3",
|
||||||
"mime-types": "^3.0.1",
|
"mime-types": "^3.0.1",
|
||||||
"minio": "^8.0.5",
|
"minio": "^8.0.5",
|
||||||
|
|
@ -3710,6 +3712,21 @@
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/lodash": {
|
||||||
|
"version": "4.17.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.17.tgz",
|
||||||
|
"integrity": "sha512-RRVJ+J3J+WmyOTqnz3PiBLA501eKwXl2noseKOrNo/6+XEHjTAxO4xHvxQB6QuNm+s4WRbn6rSiap8+EA+ykFQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/lodash-es": {
|
||||||
|
"version": "4.17.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
|
||||||
|
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/lodash": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/mailparser": {
|
"node_modules/@types/mailparser": {
|
||||||
"version": "3.4.6",
|
"version": "3.4.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mailparser/-/mailparser-3.4.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/mailparser/-/mailparser-3.4.6.tgz",
|
||||||
|
|
@ -8821,6 +8838,12 @@
|
||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash-es": {
|
||||||
|
"version": "4.17.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
|
||||||
|
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lodash.debounce": {
|
"node_modules/lodash.debounce": {
|
||||||
"version": "4.0.8",
|
"version": "4.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,11 @@
|
||||||
"postinstall": "nuxt prepare"
|
"postinstall": "nuxt prepare"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@vite-pwa/nuxt": "^0.10.6",
|
"@vite-pwa/nuxt": "^0.10.6",
|
||||||
"formidable": "^3.5.4",
|
"formidable": "^3.5.4",
|
||||||
"imap": "^0.8.19",
|
"imap": "^0.8.19",
|
||||||
|
"lodash-es": "^4.17.21",
|
||||||
"mailparser": "^3.7.3",
|
"mailparser": "^3.7.3",
|
||||||
"mime-types": "^3.0.1",
|
"mime-types": "^3.0.1",
|
||||||
"minio": "^8.0.5",
|
"minio": "^8.0.5",
|
||||||
|
|
|
||||||
|
|
@ -203,12 +203,12 @@ async function fetchImapEmails(
|
||||||
console.log(`Searching in folder: ${folderName}`);
|
console.log(`Searching in folder: ${folderName}`);
|
||||||
|
|
||||||
// Search for emails both sent and received with this client
|
// Search for emails both sent and received with this client
|
||||||
|
// Note: BCC search might not be supported by all IMAP servers
|
||||||
const searchCriteria = [
|
const searchCriteria = [
|
||||||
'OR',
|
'OR',
|
||||||
['FROM', clientEmail],
|
['FROM', clientEmail],
|
||||||
['TO', clientEmail],
|
['TO', clientEmail],
|
||||||
['CC', clientEmail],
|
['CC', clientEmail]
|
||||||
['BCC', clientEmail]
|
|
||||||
];
|
];
|
||||||
|
|
||||||
imap.search(searchCriteria, (err, results) => {
|
imap.search(searchCriteria, (err, results) => {
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,21 @@ export default defineEventHandler(async (event) => {
|
||||||
throw createError({ statusCode: 404, statusMessage: "Interest not found" });
|
throw createError({ statusCode: 404, statusMessage: "Interest not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if EOI already exists (has signature links)
|
||||||
|
if (interest['Signature Link Client'] && interest['Signature Link CC'] && interest['Signature Link Developer']) {
|
||||||
|
console.log('EOI already exists, returning existing links');
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
documentId: 'existing',
|
||||||
|
clientSigningUrl: interest['Signature Link Client'],
|
||||||
|
signingLinks: {
|
||||||
|
'Client': interest['Signature Link Client'],
|
||||||
|
'CC': interest['Signature Link CC'],
|
||||||
|
'Developer': interest['Signature Link Developer']
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Validate required fields
|
// Validate required fields
|
||||||
const requiredFields = [
|
const requiredFields = [
|
||||||
{ field: 'Full Name', value: interest['Full Name'] },
|
{ field: 'Full Name', value: interest['Full Name'] },
|
||||||
|
|
@ -260,15 +275,15 @@ export default defineEventHandler(async (event) => {
|
||||||
'Extra Comments': updatedComments
|
'Extra Comments': updatedComments
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add signing links to update data
|
// Add signing links to update data with new column names
|
||||||
if (signingLinks['Client']) {
|
if (signingLinks['Client']) {
|
||||||
updateData['EOI Client Link'] = signingLinks['Client'];
|
updateData['Signature Link Client'] = signingLinks['Client'];
|
||||||
}
|
}
|
||||||
if (signingLinks['David Mizrahi']) {
|
if (signingLinks['David Mizrahi']) {
|
||||||
updateData['EOI David Link'] = signingLinks['David Mizrahi'];
|
updateData['Signature Link Developer'] = signingLinks['David Mizrahi'];
|
||||||
}
|
}
|
||||||
if (signingLinks['Oscar Faragher']) {
|
if (signingLinks['Oscar Faragher']) {
|
||||||
updateData['EOI Oscar Link'] = signingLinks['Oscar Faragher'];
|
updateData['Signature Link CC'] = signingLinks['Oscar Faragher'];
|
||||||
}
|
}
|
||||||
|
|
||||||
await updateInterest(interestId, updateData);
|
await updateInterest(interestId, updateData);
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,11 @@ export const updateInterest = async (id: string, data: Partial<Interest>, retryC
|
||||||
"EOI Client Link",
|
"EOI Client Link",
|
||||||
"EOI David Link",
|
"EOI David Link",
|
||||||
"EOI Oscar Link",
|
"EOI Oscar Link",
|
||||||
"EOI Document"
|
"EOI Document",
|
||||||
|
// Add the new signature link fields
|
||||||
|
"Signature Link Client",
|
||||||
|
"Signature Link CC",
|
||||||
|
"Signature Link Developer"
|
||||||
];
|
];
|
||||||
|
|
||||||
// Filter the data to only include allowed fields
|
// Filter the data to only include allowed fields
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,14 @@ export interface Interest {
|
||||||
"Contract Sent Status": ContractSentStatus;
|
"Contract Sent Status": ContractSentStatus;
|
||||||
"Deposit 10% Status": Deposit10PercentStatus;
|
"Deposit 10% Status": Deposit10PercentStatus;
|
||||||
"Contract Status": ContractStatus;
|
"Contract Status": ContractStatus;
|
||||||
|
// EOI Link fields
|
||||||
|
"EOI Client Link"?: string;
|
||||||
|
"EOI David Link"?: string;
|
||||||
|
"EOI Oscar Link"?: string;
|
||||||
|
// New signature link fields
|
||||||
|
"Signature Link Client"?: string;
|
||||||
|
"Signature Link CC"?: string;
|
||||||
|
"Signature Link Developer"?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InterestsResponse {
|
export interface InterestsResponse {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue