This commit is contained in:
2025-06-10 16:48:40 +02:00
parent 49aa47ab10
commit 839b307edd
8 changed files with 473 additions and 57 deletions

View File

@@ -74,9 +74,123 @@
</div>
<!-- Email Thread List -->
<div v-if="emailThreads.length > 0" class="email-threads">
<div class="text-subtitle-1 mb-3">Email History</div>
<v-timeline :side="mobile ? 'end' : 'end'" :density="mobile ? 'compact' : 'comfortable'">
<div v-if="emailThreads.length > 0 || threads.length > 0" class="email-threads">
<div class="text-subtitle-1 mb-3 d-flex align-center">
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-for="(email, index) in emailThreads"
:key="index"
@@ -90,7 +204,13 @@
</div>
</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">
<span class="text-body-2">
{{ email.direction === 'sent' ? 'To' : 'From' }}:
@@ -103,7 +223,7 @@
<v-card-text :class="mobile ? 'pa-3' : ''">
<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 -->
<div v-if="email.attachments && email.attachments.length > 0" class="mt-3">
@@ -116,9 +236,20 @@
prepend-icon="mdi-paperclip"
class="mr-2"
>
{{ attachment.name }}
{{ attachment.name || attachment }}
</v-chip>
</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>
</v-timeline-item>
@@ -392,12 +523,19 @@
</v-card-actions>
</v-card>
</v-dialog>
<!-- Email Details Dialog -->
<EmailDetailsDialog
v-model="showEmailDetails"
:email="selectedEmail"
/>
</div>
</template>
<script setup lang="ts">
import type { Interest } from '~/utils/types';
import FileBrowser from '~/pages/dashboard/file-browser.vue';
import EmailDetailsDialog from '~/components/EmailDetailsDialog.vue';
const props = defineProps<{
interest: Interest;
@@ -423,6 +561,12 @@ const includeSignature = ref(true);
const signatureConfig = ref<any>({});
const showFileBrowser = ref(false);
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<{
subject: string;
@@ -530,7 +674,7 @@ const loadEmailThread = async () => {
// Check if we have threads from the API response
if (response.threads) {
console.log('[ClientEmailSection] Threads available:', response.threads.length);
// For now, still use emails until we implement thread UI
threads.value = response.threads;
}
}
} catch (error) {
@@ -747,6 +891,7 @@ const closeComposer = () => {
};
attachmentMode.value = 'upload';
selectedBrowserFiles.value = [];
replyingTo.value = null;
};
const removeBrowserFile = (index: number) => {
@@ -840,15 +985,86 @@ const cancelFileBrowser = () => {
showFileBrowser.value = false;
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>
<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 {
max-height: 200px;
overflow-y: auto;
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 {
max-height: 150px;
font-size: 0.875rem;

View File

@@ -85,7 +85,7 @@
:preferred-countries="['US', 'FR', 'ES', 'PT', 'GB']"
/>
</v-col>
<v-col cols="12" md="4">
<v-col cols="12" md="6">
<v-text-field
v-model="newInterest.Address"
label="Address"
@@ -94,16 +94,7 @@
prepend-inner-icon="mdi-map-marker"
></v-text-field>
</v-col>
<v-col cols="12" md="4">
<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-col cols="12" md="6">
<v-select
v-model="newInterest['Contact Method Preferred']"
label="Contact Method Preferred"
@@ -353,7 +344,6 @@ const getInitialInterest = () => ({
"Phone Number": "",
"Contact Method Preferred": "Email",
Address: "",
"Place of Residence": "",
"Yacht Name": "",
"Berth Size Desired": "",
Length: "",

View File

@@ -35,11 +35,11 @@
:loading="isGenerating"
color="primary"
variant="flat"
:prepend-icon="!mobile ? 'mdi-file-document-plus' : undefined"
:icon="mobile ? 'mdi-file-document-plus' : undefined"
prepend-icon="mdi-file-document-plus"
:size="mobile ? 'default' : 'default'"
:block="mobile"
>
<span v-if="!mobile">Generate EOI</span>
Generate EOI
</v-btn>
</div>

View File

@@ -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>

View File

@@ -1222,24 +1222,22 @@ const getSalesLevelColor = (level: string) => {
// Confirm delete
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 (debouncedDeleteInterest) {
debouncedDeleteInterest();
} else {
deleteInterest();
}
deleteInterest();
}
};
// Delete interest
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;
try {
await $fetch("/api/delete-interest", {
const response = await $fetch("/api/delete-interest", {
method: "POST",
headers: {
"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!");
closeModal();
emit("save", interest.value); // Trigger refresh
} catch (error) {
console.error("Failed to delete interest:", error);
toast.error("Failed to delete interest. Please try again.");
} catch (error: any) {
console.error("[InterestDetailsModal] Failed to delete interest:", error);
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 {
isDeleting.value = false;
}