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

291 lines
8.6 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-actions>
<v-spacer />
<v-btn
size="small"
variant="text"
color="primary"
prepend-icon="mdi-reply"
@click="replyToEmail(email, thread)"
>
Reply
</v-btn>
</v-card-actions>
</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 emit = defineEmits<{
'reply-to-email': [email: EmailMessage, thread: EmailThread];
}>();
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 () => {
// Check if we have a session ID before trying to fetch emails
const sessionId = getSessionId();
if (!sessionId) {
// No credentials, don't try to fetch
threads.value = [];
loading.value = false;
return;
}
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: sessionId,
limit: 20
}
});
if (response.success) {
threads.value = response.threads || [];
}
} catch (error: any) {
console.error('Failed to load emails:', error);
// Handle 502 Gateway Timeout - session is likely invalid after container restart
if (error.statusCode === 502 || error.status === 502) {
console.log('Got 502 error, clearing invalid session');
// Clear the invalid session
sessionStorage.removeItem('emailSessionId');
// Don't show error toast, just reset to no emails
threads.value = [];
return;
}
if (error.data?.statusMessage?.includes('Email credentials not found')) {
// Don't show error, parent component should handle reconnection
threads.value = [];
} else {
toast.error(error.data?.statusMessage || 'Failed to load email history');
}
} finally {
loading.value = false;
}
};
// Reply to an email
const replyToEmail = (email: EmailMessage, thread: EmailThread) => {
emit('reply-to-email', email, thread);
};
// 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>