updates
This commit is contained in:
parent
49aa47ab10
commit
839b307edd
|
|
@ -74,9 +74,123 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Email Thread List -->
|
<!-- Email Thread List -->
|
||||||
<div v-if="emailThreads.length > 0" class="email-threads">
|
<div v-if="emailThreads.length > 0 || threads.length > 0" class="email-threads">
|
||||||
<div class="text-subtitle-1 mb-3">Email History</div>
|
<div class="text-subtitle-1 mb-3 d-flex align-center">
|
||||||
<v-timeline :side="mobile ? 'end' : 'end'" :density="mobile ? 'compact' : 'comfortable'">
|
Email History
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn-toggle
|
||||||
|
v-model="viewMode"
|
||||||
|
mandatory
|
||||||
|
density="compact"
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<v-btn value="threads" size="small">
|
||||||
|
<v-icon :start="!mobile">mdi-forum</v-icon>
|
||||||
|
<span v-if="!mobile" class="ml-1">Threads</span>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn value="all" size="small">
|
||||||
|
<v-icon :start="!mobile">mdi-email-multiple</v-icon>
|
||||||
|
<span v-if="!mobile" class="ml-1">All</span>
|
||||||
|
</v-btn>
|
||||||
|
</v-btn-toggle>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Thread View -->
|
||||||
|
<div v-if="viewMode === 'threads' && threads.length > 0">
|
||||||
|
<v-expansion-panels v-model="expandedThreads" multiple>
|
||||||
|
<v-expansion-panel v-for="thread in threads" :key="thread.id">
|
||||||
|
<v-expansion-panel-title>
|
||||||
|
<div class="d-flex align-center justify-space-between w-100">
|
||||||
|
<div>
|
||||||
|
<div class="text-body-2 font-weight-medium">{{ thread.subject }}</div>
|
||||||
|
<div class="text-caption text-grey">
|
||||||
|
{{ thread.emailCount }} {{ thread.emailCount === 1 ? 'email' : 'emails' }} •
|
||||||
|
Last activity {{ formatRelativeTime(thread.latestTimestamp) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<v-chip size="small" color="primary" variant="tonal">
|
||||||
|
{{ thread.emailCount }}
|
||||||
|
</v-chip>
|
||||||
|
</div>
|
||||||
|
</v-expansion-panel-title>
|
||||||
|
<v-expansion-panel-text>
|
||||||
|
<v-timeline :density="mobile ? 'compact' : 'comfortable'" side="end">
|
||||||
|
<v-timeline-item
|
||||||
|
v-for="(email, emailIndex) in thread.emails"
|
||||||
|
:key="emailIndex"
|
||||||
|
:dot-color="email.direction === 'sent' ? 'primary' : 'success'"
|
||||||
|
:icon="email.direction === 'sent' ? 'mdi-email-send' : 'mdi-email-receive'"
|
||||||
|
:size="mobile ? 'x-small' : 'small'"
|
||||||
|
>
|
||||||
|
<v-card
|
||||||
|
variant="outlined"
|
||||||
|
:density="mobile ? 'compact' : 'default'"
|
||||||
|
@click="viewEmail(email)"
|
||||||
|
class="email-card"
|
||||||
|
:hover="true"
|
||||||
|
>
|
||||||
|
<v-card-subtitle class="d-flex align-center justify-space-between">
|
||||||
|
<span class="text-body-2">
|
||||||
|
{{ email.direction === 'sent' ? 'To' : 'From' }}:
|
||||||
|
{{ email.direction === 'sent' ? email.to : email.from }}
|
||||||
|
</span>
|
||||||
|
<span class="text-caption text-grey">
|
||||||
|
{{ formatDate(email.timestamp) }}
|
||||||
|
</span>
|
||||||
|
</v-card-subtitle>
|
||||||
|
|
||||||
|
<v-card-text :class="mobile ? 'pa-3' : ''">
|
||||||
|
<div class="email-content email-preview" :class="mobile ? 'email-content-mobile' : ''" v-html="formatEmailContent(email.content || email.body)"></div>
|
||||||
|
|
||||||
|
<!-- Attachments -->
|
||||||
|
<div v-if="email.attachments && email.attachments.length > 0" class="mt-3">
|
||||||
|
<v-chip
|
||||||
|
v-for="(attachment, i) in email.attachments"
|
||||||
|
:key="i"
|
||||||
|
size="small"
|
||||||
|
color="primary"
|
||||||
|
variant="tonal"
|
||||||
|
prepend-icon="mdi-paperclip"
|
||||||
|
class="mr-2"
|
||||||
|
>
|
||||||
|
{{ attachment.name || attachment }}
|
||||||
|
</v-chip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-space-between mt-3">
|
||||||
|
<v-btn variant="text" size="small" color="primary" @click.stop="viewEmail(email)">
|
||||||
|
<v-icon start>mdi-email-open</v-icon>
|
||||||
|
View
|
||||||
|
</v-btn>
|
||||||
|
<v-btn variant="text" size="small" color="primary" @click.stop="replyToEmail(email)">
|
||||||
|
<v-icon start>mdi-reply</v-icon>
|
||||||
|
Reply
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-timeline-item>
|
||||||
|
</v-timeline>
|
||||||
|
|
||||||
|
<!-- Reply to thread button -->
|
||||||
|
<div class="text-center mt-3">
|
||||||
|
<v-btn
|
||||||
|
@click="replyToThread(thread)"
|
||||||
|
color="primary"
|
||||||
|
variant="tonal"
|
||||||
|
prepend-icon="mdi-reply"
|
||||||
|
>
|
||||||
|
Reply to Thread
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</v-expansion-panel-text>
|
||||||
|
</v-expansion-panel>
|
||||||
|
</v-expansion-panels>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- All Emails View (Original) -->
|
||||||
|
<v-timeline v-else :side="mobile ? 'end' : 'end'" :density="mobile ? 'compact' : 'comfortable'">
|
||||||
<v-timeline-item
|
<v-timeline-item
|
||||||
v-for="(email, index) in emailThreads"
|
v-for="(email, index) in emailThreads"
|
||||||
:key="index"
|
:key="index"
|
||||||
|
|
@ -90,7 +204,13 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<v-card variant="outlined" :density="mobile ? 'compact' : 'default'">
|
<v-card
|
||||||
|
variant="outlined"
|
||||||
|
:density="mobile ? 'compact' : 'default'"
|
||||||
|
@click="viewEmail(email)"
|
||||||
|
class="email-card"
|
||||||
|
:hover="true"
|
||||||
|
>
|
||||||
<v-card-subtitle class="d-flex align-center justify-space-between">
|
<v-card-subtitle class="d-flex align-center justify-space-between">
|
||||||
<span class="text-body-2">
|
<span class="text-body-2">
|
||||||
{{ email.direction === 'sent' ? 'To' : 'From' }}:
|
{{ email.direction === 'sent' ? 'To' : 'From' }}:
|
||||||
|
|
@ -103,7 +223,7 @@
|
||||||
|
|
||||||
<v-card-text :class="mobile ? 'pa-3' : ''">
|
<v-card-text :class="mobile ? 'pa-3' : ''">
|
||||||
<div class="text-body-2 font-weight-medium mb-2">{{ email.subject }}</div>
|
<div class="text-body-2 font-weight-medium mb-2">{{ email.subject }}</div>
|
||||||
<div class="email-content" :class="mobile ? 'email-content-mobile' : ''" v-html="formatEmailContent(email.content)"></div>
|
<div class="email-content email-preview" :class="mobile ? 'email-content-mobile' : ''" v-html="formatEmailContent(email.content || email.body)"></div>
|
||||||
|
|
||||||
<!-- Attachments -->
|
<!-- Attachments -->
|
||||||
<div v-if="email.attachments && email.attachments.length > 0" class="mt-3">
|
<div v-if="email.attachments && email.attachments.length > 0" class="mt-3">
|
||||||
|
|
@ -116,9 +236,20 @@
|
||||||
prepend-icon="mdi-paperclip"
|
prepend-icon="mdi-paperclip"
|
||||||
class="mr-2"
|
class="mr-2"
|
||||||
>
|
>
|
||||||
{{ attachment.name }}
|
{{ attachment.name || attachment }}
|
||||||
</v-chip>
|
</v-chip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-space-between mt-3">
|
||||||
|
<v-btn variant="text" size="small" color="primary">
|
||||||
|
<v-icon start>mdi-email-open</v-icon>
|
||||||
|
View Full Email
|
||||||
|
</v-btn>
|
||||||
|
<v-btn variant="text" size="small" color="primary" @click.stop="replyToEmail(email)">
|
||||||
|
<v-icon start>mdi-reply</v-icon>
|
||||||
|
Reply
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-timeline-item>
|
</v-timeline-item>
|
||||||
|
|
@ -392,12 +523,19 @@
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
|
|
||||||
|
<!-- Email Details Dialog -->
|
||||||
|
<EmailDetailsDialog
|
||||||
|
v-model="showEmailDetails"
|
||||||
|
:email="selectedEmail"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Interest } from '~/utils/types';
|
import type { Interest } from '~/utils/types';
|
||||||
import FileBrowser from '~/pages/dashboard/file-browser.vue';
|
import FileBrowser from '~/pages/dashboard/file-browser.vue';
|
||||||
|
import EmailDetailsDialog from '~/components/EmailDetailsDialog.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
interest: Interest;
|
interest: Interest;
|
||||||
|
|
@ -423,6 +561,12 @@ const includeSignature = ref(true);
|
||||||
const signatureConfig = ref<any>({});
|
const signatureConfig = ref<any>({});
|
||||||
const showFileBrowser = ref(false);
|
const showFileBrowser = ref(false);
|
||||||
const tempSelectedFiles = ref<any[]>([]);
|
const tempSelectedFiles = ref<any[]>([]);
|
||||||
|
const showEmailDetails = ref(false);
|
||||||
|
const selectedEmail = ref<any>(null);
|
||||||
|
const threads = ref<any[]>([]);
|
||||||
|
const viewMode = ref('threads');
|
||||||
|
const expandedThreads = ref<number[]>([0]); // Expand first thread by default
|
||||||
|
const replyingTo = ref<any>(null);
|
||||||
|
|
||||||
const emailDraft = ref<{
|
const emailDraft = ref<{
|
||||||
subject: string;
|
subject: string;
|
||||||
|
|
@ -530,7 +674,7 @@ const loadEmailThread = async () => {
|
||||||
// Check if we have threads from the API response
|
// Check if we have threads from the API response
|
||||||
if (response.threads) {
|
if (response.threads) {
|
||||||
console.log('[ClientEmailSection] Threads available:', response.threads.length);
|
console.log('[ClientEmailSection] Threads available:', response.threads.length);
|
||||||
// For now, still use emails until we implement thread UI
|
threads.value = response.threads;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -747,6 +891,7 @@ const closeComposer = () => {
|
||||||
};
|
};
|
||||||
attachmentMode.value = 'upload';
|
attachmentMode.value = 'upload';
|
||||||
selectedBrowserFiles.value = [];
|
selectedBrowserFiles.value = [];
|
||||||
|
replyingTo.value = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeBrowserFile = (index: number) => {
|
const removeBrowserFile = (index: number) => {
|
||||||
|
|
@ -840,15 +985,86 @@ const cancelFileBrowser = () => {
|
||||||
showFileBrowser.value = false;
|
showFileBrowser.value = false;
|
||||||
tempSelectedFiles.value = [];
|
tempSelectedFiles.value = [];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const viewEmail = (email: any) => {
|
||||||
|
selectedEmail.value = email;
|
||||||
|
showEmailDetails.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const replyToEmail = (email: any) => {
|
||||||
|
replyingTo.value = email;
|
||||||
|
|
||||||
|
// Pre-fill the subject with Re: prefix if not already there
|
||||||
|
const subject = email.subject || '';
|
||||||
|
emailDraft.value.subject = subject.startsWith('Re:') ? subject : `Re: ${subject}`;
|
||||||
|
|
||||||
|
// Pre-fill with a reply header
|
||||||
|
const originalDate = formatDate(email.timestamp);
|
||||||
|
const originalFrom = email.direction === 'sent' ? 'you' : email.from;
|
||||||
|
|
||||||
|
emailDraft.value.content = `\n\n\n--- Original Message ---\nFrom: ${originalFrom}\nDate: ${originalDate}\n\n${email.content || email.body || ''}`;
|
||||||
|
|
||||||
|
showComposer.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const replyToThread = (thread: any) => {
|
||||||
|
// Reply to the last email in the thread
|
||||||
|
const lastEmail = thread.emails[thread.emails.length - 1];
|
||||||
|
replyToEmail(lastEmail);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatRelativeTime = (timestamp: string) => {
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
const now = new Date();
|
||||||
|
const diff = now.getTime() - date.getTime();
|
||||||
|
const minutes = Math.floor(diff / 60000);
|
||||||
|
const hours = Math.floor(diff / 3600000);
|
||||||
|
const days = Math.floor(diff / 86400000);
|
||||||
|
|
||||||
|
if (minutes < 1) return 'just now';
|
||||||
|
if (minutes < 60) return `${minutes}m ago`;
|
||||||
|
if (hours < 24) return `${hours}h ago`;
|
||||||
|
if (days < 7) return `${days}d ago`;
|
||||||
|
|
||||||
|
return date.toLocaleDateString('en-GB', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric'
|
||||||
|
});
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.email-card {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
.email-content {
|
.email-content {
|
||||||
max-height: 200px;
|
|
||||||
overflow-y: auto;
|
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.email-preview {
|
||||||
|
max-height: 100px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-preview::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 30px;
|
||||||
|
background: linear-gradient(to bottom, transparent, white);
|
||||||
|
}
|
||||||
|
|
||||||
.email-content-mobile {
|
.email-content-mobile {
|
||||||
max-height: 150px;
|
max-height: 150px;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@
|
||||||
:preferred-countries="['US', 'FR', 'ES', 'PT', 'GB']"
|
:preferred-countries="['US', 'FR', 'ES', 'PT', 'GB']"
|
||||||
/>
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" md="4">
|
<v-col cols="12" md="6">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="newInterest.Address"
|
v-model="newInterest.Address"
|
||||||
label="Address"
|
label="Address"
|
||||||
|
|
@ -94,16 +94,7 @@
|
||||||
prepend-inner-icon="mdi-map-marker"
|
prepend-inner-icon="mdi-map-marker"
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" md="4">
|
<v-col cols="12" md="6">
|
||||||
<v-text-field
|
|
||||||
v-model="newInterest['Place of Residence']"
|
|
||||||
label="Place of Residence"
|
|
||||||
variant="outlined"
|
|
||||||
density="comfortable"
|
|
||||||
prepend-inner-icon="mdi-home"
|
|
||||||
></v-text-field>
|
|
||||||
</v-col>
|
|
||||||
<v-col cols="12" md="4">
|
|
||||||
<v-select
|
<v-select
|
||||||
v-model="newInterest['Contact Method Preferred']"
|
v-model="newInterest['Contact Method Preferred']"
|
||||||
label="Contact Method Preferred"
|
label="Contact Method Preferred"
|
||||||
|
|
@ -353,7 +344,6 @@ const getInitialInterest = () => ({
|
||||||
"Phone Number": "",
|
"Phone Number": "",
|
||||||
"Contact Method Preferred": "Email",
|
"Contact Method Preferred": "Email",
|
||||||
Address: "",
|
Address: "",
|
||||||
"Place of Residence": "",
|
|
||||||
"Yacht Name": "",
|
"Yacht Name": "",
|
||||||
"Berth Size Desired": "",
|
"Berth Size Desired": "",
|
||||||
Length: "",
|
Length: "",
|
||||||
|
|
|
||||||
|
|
@ -35,11 +35,11 @@
|
||||||
:loading="isGenerating"
|
:loading="isGenerating"
|
||||||
color="primary"
|
color="primary"
|
||||||
variant="flat"
|
variant="flat"
|
||||||
:prepend-icon="!mobile ? 'mdi-file-document-plus' : undefined"
|
prepend-icon="mdi-file-document-plus"
|
||||||
:icon="mobile ? 'mdi-file-document-plus' : undefined"
|
|
||||||
:size="mobile ? 'default' : 'default'"
|
:size="mobile ? 'default' : 'default'"
|
||||||
|
:block="mobile"
|
||||||
>
|
>
|
||||||
<span v-if="!mobile">Generate EOI</span>
|
Generate EOI
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,149 @@
|
||||||
|
<template>
|
||||||
|
<v-dialog
|
||||||
|
v-model="isOpen"
|
||||||
|
:max-width="mobile ? '100%' : '800'"
|
||||||
|
:fullscreen="mobile"
|
||||||
|
:transition="mobile ? 'dialog-bottom-transition' : 'dialog-transition'"
|
||||||
|
>
|
||||||
|
<v-card v-if="email">
|
||||||
|
<v-card-title class="d-flex align-center">
|
||||||
|
<v-icon class="mr-2" :color="email.direction === 'sent' ? 'primary' : 'success'">
|
||||||
|
{{ email.direction === 'sent' ? 'mdi-email-send' : 'mdi-email-receive' }}
|
||||||
|
</v-icon>
|
||||||
|
Email Details
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn icon="mdi-close" variant="text" @click="close"></v-btn>
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-divider />
|
||||||
|
|
||||||
|
<v-card-text>
|
||||||
|
<v-list density="comfortable">
|
||||||
|
<v-list-item>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon>mdi-account</v-icon>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>{{ email.direction === 'sent' ? 'To' : 'From' }}</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ email.direction === 'sent' ? email.to : email.from }}</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon>mdi-text-box</v-icon>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>Subject</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ email.subject }}</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<v-list-item>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon>mdi-calendar-clock</v-icon>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>Date & Time</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ formatDate(email.timestamp) }}</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
|
||||||
|
<v-divider class="my-4" />
|
||||||
|
|
||||||
|
<div class="email-body">
|
||||||
|
<div class="text-subtitle-2 mb-2">Message</div>
|
||||||
|
<div class="email-content" v-html="formatEmailContent(email.content || email.body)"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Attachments -->
|
||||||
|
<div v-if="email.attachments && email.attachments.length > 0" class="mt-4">
|
||||||
|
<div class="text-subtitle-2 mb-2">Attachments ({{ email.attachments.length }})</div>
|
||||||
|
<v-chip
|
||||||
|
v-for="(attachment, i) in email.attachments"
|
||||||
|
:key="i"
|
||||||
|
size="small"
|
||||||
|
color="primary"
|
||||||
|
variant="tonal"
|
||||||
|
prepend-icon="mdi-paperclip"
|
||||||
|
class="mr-2 mb-2"
|
||||||
|
>
|
||||||
|
{{ attachment.name || attachment }}
|
||||||
|
</v-chip>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-divider />
|
||||||
|
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn @click="close" variant="text">Close</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue: boolean;
|
||||||
|
email: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
const emit = defineEmits(['update:modelValue']);
|
||||||
|
|
||||||
|
const { mobile } = useDisplay();
|
||||||
|
|
||||||
|
const isOpen = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (value) => emit('update:modelValue', value)
|
||||||
|
});
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
isOpen.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateString: string) => {
|
||||||
|
if (!dateString) return '';
|
||||||
|
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleString('en-GB', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: false
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatEmailContent = (content: string) => {
|
||||||
|
if (!content) return '';
|
||||||
|
|
||||||
|
// If it's already HTML, return as is
|
||||||
|
if (content.includes('<p>') || content.includes('<br>')) {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert plain text to HTML
|
||||||
|
return content
|
||||||
|
.split('\n')
|
||||||
|
.map(line => line.trim() ? `<p>${line}</p>` : '<br>')
|
||||||
|
.join('');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.email-content {
|
||||||
|
font-family: inherit;
|
||||||
|
line-height: 1.6;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-content :deep(p) {
|
||||||
|
margin: 0 0 0.5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-content :deep(br) {
|
||||||
|
display: block;
|
||||||
|
content: "";
|
||||||
|
margin: 0.25em 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1222,24 +1222,22 @@ const getSalesLevelColor = (level: string) => {
|
||||||
|
|
||||||
// Confirm delete
|
// Confirm delete
|
||||||
const confirmDelete = () => {
|
const confirmDelete = () => {
|
||||||
if (!interest.value) return;
|
if (!interest.value || isDeleting.value) return;
|
||||||
|
|
||||||
if (confirm(`Are you sure you want to delete the interest for ${interest.value['Full Name']}? This action cannot be undone.`)) {
|
if (confirm(`Are you sure you want to delete the interest for ${interest.value['Full Name']}? This action cannot be undone.`)) {
|
||||||
if (debouncedDeleteInterest) {
|
deleteInterest();
|
||||||
debouncedDeleteInterest();
|
|
||||||
} else {
|
|
||||||
deleteInterest();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Delete interest
|
// Delete interest
|
||||||
const deleteInterest = async () => {
|
const deleteInterest = async () => {
|
||||||
if (!interest.value) return;
|
if (!interest.value || isDeleting.value) return;
|
||||||
|
|
||||||
|
console.log('[InterestDetailsModal] Starting delete for interest:', interest.value.Id);
|
||||||
isDeleting.value = true;
|
isDeleting.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await $fetch("/api/delete-interest", {
|
const response = await $fetch("/api/delete-interest", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"x-tag": user.value?.email ? "094ut234" : "pjnvü1230",
|
"x-tag": user.value?.email ? "094ut234" : "pjnvü1230",
|
||||||
|
|
@ -1249,12 +1247,15 @@ const deleteInterest = async () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('[InterestDetailsModal] Delete response:', response);
|
||||||
toast.success("Interest deleted successfully!");
|
toast.success("Interest deleted successfully!");
|
||||||
closeModal();
|
closeModal();
|
||||||
emit("save", interest.value); // Trigger refresh
|
emit("save", interest.value); // Trigger refresh
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error("Failed to delete interest:", error);
|
console.error("[InterestDetailsModal] Failed to delete interest:", error);
|
||||||
toast.error("Failed to delete interest. Please try again.");
|
console.error("[InterestDetailsModal] Error details:", error.data || error.message);
|
||||||
|
const errorMessage = error.data?.statusMessage || error.message || "Failed to delete interest. Please try again.";
|
||||||
|
toast.error(errorMessage);
|
||||||
} finally {
|
} finally {
|
||||||
isDeleting.value = false;
|
isDeleting.value = false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -585,37 +585,51 @@ const getRelativeTime = (dateString: string) => {
|
||||||
/* Mobile-specific styles */
|
/* Mobile-specific styles */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.table-container {
|
.table-container {
|
||||||
margin: 0 -12px;
|
position: relative;
|
||||||
padding: 0 12px;
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
margin: 0 -16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* For mobile, only show essential columns */
|
/* Add padding to the wrapper instead */
|
||||||
.modern-table :deep(.v-table__wrapper) {
|
.modern-table :deep(.v-table__wrapper) {
|
||||||
min-width: auto;
|
padding: 0 16px;
|
||||||
|
min-width: 600px; /* Minimum width to ensure scrolling */
|
||||||
}
|
}
|
||||||
|
|
||||||
.modern-table :deep(th) {
|
.modern-table :deep(th) {
|
||||||
padding: 8px !important;
|
padding: 8px !important;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modern-table :deep(td) {
|
.modern-table :deep(td) {
|
||||||
padding: 12px 8px !important;
|
padding: 12px 8px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hide columns on mobile that aren't in mobile headers */
|
/* Show all columns but with smaller widths on mobile */
|
||||||
.modern-table :deep(th:nth-child(n+4)),
|
.modern-table :deep(th:nth-child(1)),
|
||||||
.modern-table :deep(td:nth-child(n+4)) {
|
.modern-table :deep(td:nth-child(1)) {
|
||||||
display: none;
|
min-width: 180px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modern-table :deep(th:nth-child(2)),
|
||||||
|
.modern-table :deep(td:nth-child(2)) {
|
||||||
|
min-width: 120px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modern-table :deep(th:nth-child(3)),
|
||||||
|
.modern-table :deep(td:nth-child(3)) {
|
||||||
|
min-width: 100px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Contact cell optimization */
|
/* Contact cell optimization */
|
||||||
.contact-cell {
|
.contact-cell {
|
||||||
max-width: 200px;
|
max-width: 180px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.contact-cell .text-truncate {
|
.contact-cell .text-truncate {
|
||||||
max-width: 150px;
|
max-width: 140px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Adjust table row height on mobile */
|
/* Adjust table row height on mobile */
|
||||||
|
|
@ -628,6 +642,18 @@ const getRelativeTime = (dateString: string) => {
|
||||||
height: 20px !important;
|
height: 20px !important;
|
||||||
font-size: 0.625rem !important;
|
font-size: 0.625rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Add visual scroll indicators */
|
||||||
|
.table-container::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 30px;
|
||||||
|
background: linear-gradient(to right, transparent, rgba(255,255,255,0.8));
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure proper text truncation */
|
/* Ensure proper text truncation */
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,16 @@ export default defineEventHandler(async (event) => {
|
||||||
|
|
||||||
// Update interest with EOI document information
|
// Update interest with EOI document information
|
||||||
console.log('[EOI Upload] Updating interest with EOI document info');
|
console.log('[EOI Upload] Updating interest with EOI document info');
|
||||||
await updateInterestEOIDocument(interestId, documentData);
|
console.log('[EOI Upload] Document data:', JSON.stringify(documentData, null, 2));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateInterestEOIDocument(interestId, documentData);
|
||||||
|
console.log('[EOI Upload] Successfully updated EOI document in database');
|
||||||
|
} catch (dbError: any) {
|
||||||
|
console.error('[EOI Upload] Failed to update database with EOI document:', dbError);
|
||||||
|
console.error('[EOI Upload] Database error details:', dbError.data || dbError.message);
|
||||||
|
throw new Error(`Failed to update database: ${dbError.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Update the status fields for uploaded (signed) EOI
|
// Update the status fields for uploaded (signed) EOI
|
||||||
const updateData: any = {
|
const updateData: any = {
|
||||||
|
|
@ -116,18 +125,26 @@ export default defineEventHandler(async (event) => {
|
||||||
'Sales Process Level': 'Signed LOI and NDA'
|
'Sales Process Level': 'Signed LOI and NDA'
|
||||||
};
|
};
|
||||||
console.log('[EOI Upload] Updating interest status fields for signed EOI');
|
console.log('[EOI Upload] Updating interest status fields for signed EOI');
|
||||||
|
console.log('[EOI Upload] Status update data:', JSON.stringify(updateData, null, 2));
|
||||||
|
|
||||||
// Update the interest
|
try {
|
||||||
await $fetch('/api/update-interest', {
|
// Update the interest
|
||||||
method: 'POST',
|
await $fetch('/api/update-interest', {
|
||||||
headers: {
|
method: 'POST',
|
||||||
'x-tag': xTagHeader,
|
headers: {
|
||||||
},
|
'x-tag': xTagHeader,
|
||||||
body: {
|
},
|
||||||
id: interestId,
|
body: {
|
||||||
data: updateData
|
id: interestId,
|
||||||
}
|
data: updateData
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
console.log('[EOI Upload] Successfully updated interest status');
|
||||||
|
} catch (statusError: any) {
|
||||||
|
console.error('[EOI Upload] Failed to update interest status:', statusError);
|
||||||
|
console.error('[EOI Upload] Status error details:', statusError.data || statusError.message);
|
||||||
|
// Don't throw here - the file was uploaded successfully
|
||||||
|
}
|
||||||
|
|
||||||
console.log('[EOI Upload] Upload completed successfully');
|
console.log('[EOI Upload] Upload completed successfully');
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,20 @@ import { promises as fs } from 'fs';
|
||||||
import mime from 'mime-types';
|
import mime from 'mime-types';
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
|
const xTagHeader = getRequestHeader(event, "x-tag");
|
||||||
|
|
||||||
|
if (!xTagHeader || (xTagHeader !== "094ut234" && xTagHeader !== "pjnvü1230")) {
|
||||||
|
throw createError({ statusCode: 401, statusMessage: "unauthenticated" });
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get the current path and bucket from query params
|
// Get the current path and bucket from query params
|
||||||
const query = getQuery(event);
|
const query = getQuery(event);
|
||||||
const currentPath = (query.path as string) || '';
|
const currentPath = (query.path as string) || '';
|
||||||
const bucket = (query.bucket as string) || 'client-portal'; // Default bucket
|
const bucket = (query.bucket as string) || 'client-portal'; // Default bucket
|
||||||
|
|
||||||
|
console.log('[Upload] Request received for bucket:', bucket, 'path:', currentPath);
|
||||||
|
|
||||||
// Parse multipart form data
|
// Parse multipart form data
|
||||||
const form = formidable({
|
const form = formidable({
|
||||||
maxFileSize: 50 * 1024 * 1024, // 50MB limit
|
maxFileSize: 50 * 1024 * 1024, // 50MB limit
|
||||||
|
|
@ -50,6 +58,15 @@ export default defineEventHandler(async (event) => {
|
||||||
} else {
|
} else {
|
||||||
// For other buckets, use the MinIO client directly
|
// For other buckets, use the MinIO client directly
|
||||||
const client = getMinioClient();
|
const client = getMinioClient();
|
||||||
|
|
||||||
|
// Ensure bucket exists
|
||||||
|
try {
|
||||||
|
await client.bucketExists(bucket);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`[Upload] Bucket ${bucket} doesn't exist, creating it...`);
|
||||||
|
await client.makeBucket(bucket, 'us-east-1');
|
||||||
|
}
|
||||||
|
|
||||||
await client.putObject(bucket, fullPath, fileBuffer, fileBuffer.length, {
|
await client.putObject(bucket, fullPath, fileBuffer, fileBuffer.length, {
|
||||||
'Content-Type': contentType,
|
'Content-Type': contentType,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue