This commit is contained in:
Viet An
2026-06-20 14:48:35 +07:00
parent 7402e61906
commit b596ea0a0f
31 changed files with 5911 additions and 13522 deletions

View File

@@ -1,347 +1,5 @@
<template>
<div class="container">
<div class="level mb-4">
<div class="level-left">
<div class="level-item">
<div>
<h2 class="title is-4">Sent Emails</h2>
<p class="subtitle is-6">
{{ loading ? "Loading..." : `${emailsSent.length} email(s) sent` }}
</p>
</div>
</div>
</div>
<div class="level-right">
<div class="level-item">
<button
class="button is-light"
@click="getDataEmailsSent"
:disabled="loading"
:class="{ 'is-loading': loading }"
>
<span class="icon">
<RefreshCw :size="16" />
</span>
<span>Refresh</span>
</button>
</div>
</div>
</div>
<div class="box">
<table class="table is-fullwidth is-striped is-hoverable">
<thead>
<tr>
<th>Emails</th>
<th>Subject</th>
<th>Content</th>
<th>Status</th>
<th>Sent At</th>
<th>Updated At</th>
</tr>
</thead>
<tbody>
<template v-if="loading">
<tr v-for="index in 5" :key="index">
<td><div class="skeleton-line"></div></td>
<td><div class="skeleton-line"></div></td>
<td><div class="skeleton-line"></div></td>
<td><div class="skeleton-line"></div></td>
<td><div class="skeleton-line"></div></td>
<td><div class="skeleton-line"></div></td>
</tr>
</template>
<template v-else-if="emailsSent.length === 0">
<tr>
<td colspan="6">
<div class="has-text-centered py-6">
<Mail :size="64" class="has-text-grey-light mb-4" />
<p class="title is-5">No emails sent yet</p>
<p class="subtitle is-6">Sent emails will appear here</p>
</div>
</td>
</tr>
</template>
<template v-else>
<tr v-for="email in emailsSent" :key="email.id" class="is-clickable" @click="handleViewDetail(email)">
<td>
<span>{{ truncateText(email.receiver, 40) }}</span>
</td>
<td>{{ truncateText(email.subject, 20) }}</td>
<td>
<div v-html="truncateText(stripHtml(extractMessageContent(email.content), 1000), 40)"></div>
</td>
<td>
<span v-if="email.status === 1" class="tag is-warning">
<span class="icon is-small">
<SvgIcon v-bind="{ name: 'loading.svg', type: 'white', size: 12 }" />
</span>
<span>Pending</span>
</span>
<span v-else-if="email.status === 2" class="tag is-success">
<span class="icon is-small">
<CheckCircle2 :size="12" />
</span>
<span>Sent</span>
</span>
<span v-else-if="email.status === 3" class="tag is-danger">
<span class="icon is-small">
<XCircle :size="12" />
</span>
<span>Failed</span>
</span>
<span v-else-if="email.status === 4" class="tag is-info">
<span class="icon is-small">
<Clock :size="12" />
</span>
<span>Scheduled</span>
</span>
</td>
<td>
<div>
<p class="is-size-7">
<span class="icon is-small">
<Calendar :size="12" />
</span>
{{ formatDate(email.create_time) }}
</p>
<p class="is-size-7 has-text-grey">{{ getRelativeTime(email.create_time) }}</p>
</div>
</td>
<td>
<div>
<p class="is-size-7">
<span class="icon is-small">
<Clock :size="12" />
</span>
{{ formatDate(email.update_time || "") }}
</p>
<p class="is-size-7 has-text-grey">{{ getRelativeTime(email.update_time || "") || "---" }}</p>
</div>
</td>
</tr>
</template>
</tbody>
</table>
</div>
<!-- Detail Modal -->
<div v-if="showDetailModal && selectedEmail" class="modal is-active">
<div class="modal-background" @click="showDetailModal = false"></div>
<div class="modal-card" style="width: 90%; max-width: 1200px">
<header class="modal-card-head">
<p class="modal-card-title">Email Details</p>
<button class="delete" @click="showDetailModal = false"></button>
</header>
<section class="modal-card-body">
<div class="field">
<label class="label">
Recipients ({{ selectedEmail.receiver.split(";").filter((e) => e.trim()).length }})
</label>
<div class="box" style="max-height: 150px; overflow-y: auto">
<p style="white-space: pre-wrap">{{ selectedEmail.receiver }}</p>
</div>
</div>
<div class="field">
<label class="label">Subject</label>
<p>{{ selectedEmail.subject }}</p>
</div>
<div class="field">
<label class="label">Content</label>
<div
class="box content"
style="max-height: 300px; overflow-y: auto"
v-html="selectedEmail.content || 'No content'"
></div>
</div>
<div class="columns">
<div class="column">
<label class="label">Status</label>
<div>
<span v-if="selectedEmail.status === 1" class="tag is-warning">Pending</span>
<span v-else-if="selectedEmail.status === 2" class="tag is-success">Sent</span>
<span v-else-if="selectedEmail.status === 3" class="tag is-danger">Failed</span>
<span v-else-if="selectedEmail.status === 4" class="tag is-info">Scheduled</span>
</div>
</div>
<div class="column">
<label class="label">Sent At</label>
<p>{{ new Date(selectedEmail.create_time).toLocaleString("vi-VN") }}</p>
</div>
<div class="column">
<label class="label">Updated At</label>
<p>
{{ selectedEmail.update_time ? new Date(selectedEmail.update_time).toLocaleString("vi-VN") : "---" }}
</p>
</div>
</div>
</section>
</div>
</div>
</div>
<div class="container"></div>
</template>
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { formatDistanceToNow } from "date-fns";
import axios from "axios";
import { useNuxtApp } from "nuxt/app";
import { apiUrl } from '@/components/marketing/email/Email.utils';
interface EmailSent {
id: number;
receiver: string;
subject: string;
content: string;
status: number;
create_time: string;
update_time?: string;
}
const nuxtApp = useNuxtApp();
const $snackbar = nuxtApp.$snackbar as (message?: string) => void;
const emailsSent = ref<EmailSent[]>([]);
const loading = ref(true);
const selectedEmail = ref<EmailSent | null>(null);
const showDetailModal = ref(false);
const extractMessageContent = (html: string | null | undefined): string => {
if (!html || html === "") return "No content";
try {
if (typeof window !== "undefined") {
const parser = new DOMParser();
const doc = parser.parseFromString(html, "text/html");
const messageContentDiv = doc.querySelector("div.message-content");
if (messageContentDiv) {
const content = messageContentDiv.innerHTML.trim();
if (content) {
return content;
}
return messageContentDiv.innerHTML;
}
} else {
const regex = /<div[^>]*class=["'][^"']*message-content[^"']*["'][^>]*>([\s\S]*?)<\/div>/i;
const match = html.match(regex);
if (match && match[1]) {
return match[1].trim();
}
}
} catch (error) {
console.error("Error extracting message content:", error);
}
return html;
};
const stripHtml = (html: string, maxLength: number = 1000): string => {
if (!html) return "";
const text = html.replace(/<[^>]*>/g, "");
return text.length > maxLength ? text.substring(0, maxLength) + "..." : text;
};
const getDataEmailsSent = async () => {
loading.value = true;
try {
const response = await axios.get(`${apiUrl}/Email_Sent/?sort=-id`);
if (response.status === 200) {
emailsSent.value = response.data.rows || response.data || [];
}
} catch (error) {
console.error("Error fetching sent emails:", error);
$snackbar("Failed to load sent emails");
} finally {
loading.value = false;
}
};
const formatDate = (dateString: string) => {
try {
if (dateString === "") return "";
const date = new Date(dateString);
return date.toLocaleString("vi-VN", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
});
} catch {
return "Invalid date";
}
};
const getRelativeTime = (dateString: string) => {
try {
if (dateString === "") return "";
return formatDistanceToNow(new Date(dateString), { addSuffix: true });
} catch {
return "";
}
};
const handleViewDetail = (email: EmailSent) => {
selectedEmail.value = email;
showDetailModal.value = true;
};
const truncateText = (text: string | undefined | null, maxLength: number = 50): string => {
if (!text) return "";
const textStr = String(text);
if (textStr.length <= maxLength) return textStr;
return textStr.substring(0, maxLength) + "...";
};
onMounted(() => {
getDataEmailsSent();
});
</script>
<style scoped>
.is-clickable {
cursor: pointer;
}
.is-clickable:hover {
background-color: #f5f5f5;
}
.skeleton-line {
height: 20px;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: loading 1.5s infinite;
border-radius: 4px;
}
@keyframes loading {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
.rotating {
animation: rotate 1s linear infinite;
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
</style>
<script setup lang="ts"></script>