250 lines
7.3 KiB
Vue
250 lines
7.3 KiB
Vue
<template>
|
|
<v-card>
|
|
<v-card-title class="text-h6 d-flex align-center">
|
|
<v-icon class="mr-2">mdi-email-multiple</v-icon>
|
|
Email History
|
|
<v-spacer />
|
|
<v-btn
|
|
icon="mdi-refresh"
|
|
size="small"
|
|
variant="text"
|
|
@click="loadEmails"
|
|
:loading="loading"
|
|
/>
|
|
</v-card-title>
|
|
<v-card-text>
|
|
<div v-if="loading && threads.length === 0" class="text-center py-8">
|
|
<v-progress-circular indeterminate color="primary" />
|
|
<div class="mt-2">Loading email threads...</div>
|
|
</div>
|
|
|
|
<div v-else-if="!loading && threads.length === 0" class="text-center py-8 text-grey">
|
|
<v-icon size="48" class="mb-2">mdi-email-off</v-icon>
|
|
<div>No email conversations found with this client.</div>
|
|
</div>
|
|
|
|
<v-expansion-panels v-else variant="accordion">
|
|
<v-expansion-panel v-for="thread in threads" :key="thread.id">
|
|
<v-expansion-panel-title>
|
|
<div class="d-flex align-center" style="width: 100%">
|
|
<v-icon class="mr-2">
|
|
{{ thread.emails.length > 1 ? 'mdi-email-multiple' : 'mdi-email' }}
|
|
</v-icon>
|
|
<div class="flex-grow-1">
|
|
<div class="font-weight-medium">{{ thread.subject }}</div>
|
|
<div class="text-caption text-grey">
|
|
{{ thread.emailCount }} {{ thread.emailCount === 1 ? 'email' : 'emails' }} ·
|
|
Last: {{ formatDate(thread.latestTimestamp) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</v-expansion-panel-title>
|
|
<v-expansion-panel-text>
|
|
<v-timeline density="compact" side="end">
|
|
<v-timeline-item
|
|
v-for="email in thread.emails"
|
|
:key="email.id"
|
|
:dot-color="email.direction === 'sent' ? 'primary' : 'grey'"
|
|
size="small"
|
|
>
|
|
<template v-slot:opposite>
|
|
<div class="text-caption text-grey">
|
|
{{ formatDate(email.timestamp) }}
|
|
</div>
|
|
</template>
|
|
<v-card variant="outlined">
|
|
<v-card-title class="text-subtitle-2 d-flex align-center">
|
|
<v-icon
|
|
size="small"
|
|
class="mr-1"
|
|
:color="email.direction === 'sent' ? 'primary' : 'grey'"
|
|
>
|
|
{{ email.direction === 'sent' ? 'mdi-send' : 'mdi-email-receive' }}
|
|
</v-icon>
|
|
{{ email.direction === 'sent' ? 'You' : extractName(email.from) }}
|
|
</v-card-title>
|
|
<v-card-subtitle class="text-caption">
|
|
From: {{ email.from }}<br>
|
|
To: {{ email.to }}
|
|
</v-card-subtitle>
|
|
<v-card-text>
|
|
<div
|
|
v-if="!expandedEmails[email.id]"
|
|
class="email-preview"
|
|
@click="expandedEmails[email.id] = true"
|
|
style="cursor: pointer"
|
|
>
|
|
{{ truncateText(email.body) }}
|
|
<span v-if="email.body.length > 200" class="text-primary">
|
|
... Show more
|
|
</span>
|
|
</div>
|
|
<div v-else class="email-full">
|
|
<pre style="white-space: pre-wrap; font-family: inherit;">{{ email.body }}</pre>
|
|
<div
|
|
class="text-primary text-caption mt-2"
|
|
style="cursor: pointer"
|
|
@click="expandedEmails[email.id] = false"
|
|
>
|
|
Show less
|
|
</div>
|
|
</div>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-timeline-item>
|
|
</v-timeline>
|
|
</v-expansion-panel-text>
|
|
</v-expansion-panel>
|
|
</v-expansion-panels>
|
|
</v-card-text>
|
|
</v-card>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
import { ref, onMounted, reactive } from 'vue';
|
|
import type { Interest } from '@/utils/types';
|
|
|
|
interface Props {
|
|
interest: Interest;
|
|
}
|
|
|
|
interface EmailMessage {
|
|
id: string;
|
|
from: string;
|
|
to: string | string[];
|
|
subject: string;
|
|
body: string;
|
|
html?: string;
|
|
timestamp: string;
|
|
direction: 'sent' | 'received';
|
|
threadId?: string;
|
|
}
|
|
|
|
interface EmailThread {
|
|
id: string;
|
|
subject: string;
|
|
emailCount: number;
|
|
latestTimestamp: string;
|
|
emails: EmailMessage[];
|
|
}
|
|
|
|
const props = defineProps<Props>();
|
|
|
|
const user = useDirectusUser();
|
|
const toast = useToast();
|
|
|
|
const loading = ref(false);
|
|
const threads = ref<EmailThread[]>([]);
|
|
const expandedEmails = reactive<Record<string, boolean>>({});
|
|
|
|
const getSessionId = () => {
|
|
return sessionStorage.getItem('emailSessionId') || '';
|
|
};
|
|
|
|
const formatDate = (dateString: string) => {
|
|
const date = new Date(dateString);
|
|
const now = new Date();
|
|
const diff = now.getTime() - date.getTime();
|
|
const hours = Math.floor(diff / (1000 * 60 * 60));
|
|
const days = Math.floor(hours / 24);
|
|
|
|
if (hours < 1) {
|
|
return 'Just now';
|
|
} else if (hours < 24) {
|
|
return `${hours} hour${hours === 1 ? '' : 's'} ago`;
|
|
} else if (days < 7) {
|
|
return `${days} day${days === 1 ? '' : 's'} ago`;
|
|
} else {
|
|
return date.toLocaleDateString('en-US', {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
}
|
|
};
|
|
|
|
const extractName = (email: string) => {
|
|
// Extract name from email format "Name <email@domain.com>"
|
|
const match = email.match(/^([^<]+)\s*</);
|
|
if (match) {
|
|
return match[1].trim();
|
|
}
|
|
// If no name, just return the email address
|
|
return email.replace(/<|>/g, '').trim();
|
|
};
|
|
|
|
const truncateText = (text: string, maxLength: number = 200) => {
|
|
if (text.length <= maxLength) return text;
|
|
return text.substring(0, maxLength).trim();
|
|
};
|
|
|
|
const loadEmails = async () => {
|
|
loading.value = true;
|
|
|
|
try {
|
|
const response = await $fetch<{
|
|
success: boolean;
|
|
emails: EmailMessage[];
|
|
threads: EmailThread[]
|
|
}>('/api/email/fetch-thread', {
|
|
method: 'POST',
|
|
headers: {
|
|
'x-tag': user.value?.email ? '094ut234' : 'pjnvü1230',
|
|
},
|
|
body: {
|
|
clientEmail: props.interest['Email Address'],
|
|
interestId: props.interest.Id,
|
|
sessionId: getSessionId(),
|
|
limit: 50
|
|
}
|
|
});
|
|
|
|
if (response.success) {
|
|
threads.value = response.threads;
|
|
}
|
|
} catch (error: any) {
|
|
console.error('Failed to load emails:', error);
|
|
if (error.data?.statusMessage?.includes('Email credentials not found')) {
|
|
// Don't show error, parent component should handle reconnection
|
|
} else {
|
|
toast.error(error.data?.statusMessage || 'Failed to load email history');
|
|
}
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
// Reload emails when an email is sent
|
|
const reloadEmails = () => {
|
|
loadEmails();
|
|
};
|
|
|
|
// Load emails on mount
|
|
onMounted(() => {
|
|
loadEmails();
|
|
});
|
|
|
|
// Expose reload method to parent
|
|
defineExpose({
|
|
reloadEmails
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
.email-preview {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 3;
|
|
-webkit-box-orient: vertical;
|
|
}
|
|
|
|
.email-full pre {
|
|
margin: 0;
|
|
font-size: 0.875rem;
|
|
line-height: 1.5;
|
|
}
|
|
</style>
|