changes
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user