changes
This commit is contained in:
@@ -187,7 +187,7 @@
|
||||
import { useNuxtApp } from "nuxt/app";
|
||||
import { ref, computed, onMounted, watch, markRaw } from "vue";
|
||||
import EmailForm1 from "./forms/EmailForm1.vue";
|
||||
import Template1 from "~/lib/email/templates/Template1.vue";
|
||||
import Template1 from "~/components/marketing/email/Template1.vue";
|
||||
import Modal from "~/components/Modal.vue";
|
||||
import MappingConfigurator from "~/components/marketing/email/MappingConfigurator.vue";
|
||||
import JobConfigurator from "~/components/marketing/email/JobConfigurator.vue";
|
||||
|
||||
453
app/components/marketing/email/Template1.vue
Normal file
453
app/components/marketing/email/Template1.vue
Normal file
@@ -0,0 +1,453 @@
|
||||
<template>
|
||||
<div v-if="previewMode">
|
||||
<component
|
||||
:is="'style'"
|
||||
v-html="styles"
|
||||
/>
|
||||
<div
|
||||
class="container py-6"
|
||||
:style="{
|
||||
minHeight: '100%',
|
||||
backgroundColor: 'hsl(0, 0%, 97%)',
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif',
|
||||
}"
|
||||
>
|
||||
<table
|
||||
role="presentation"
|
||||
cellspacing="0"
|
||||
cellpadding="0"
|
||||
border="0"
|
||||
:style="{
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
margin: '0 auto',
|
||||
}"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<table
|
||||
role="presentation"
|
||||
class="email-container gmail-fix"
|
||||
cellspacing="0"
|
||||
cellpadding="0"
|
||||
border="0"
|
||||
:style="{
|
||||
margin: '0 auto',
|
||||
width: '100%',
|
||||
maxWidth: '680px',
|
||||
backgroundColor: '#ffffff',
|
||||
borderRadius: '16px',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
|
||||
overflow: 'hidden',
|
||||
}"
|
||||
>
|
||||
<tbody>
|
||||
<tr class="header-row">
|
||||
<td :style="{ padding: '0' }">
|
||||
<div
|
||||
class="header-image"
|
||||
:style="{
|
||||
background: '#f3f4f6',
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
height: '425px',
|
||||
overflow: 'hidden',
|
||||
}"
|
||||
>
|
||||
<img
|
||||
v-if="hasValidImage"
|
||||
:src="content.imageUrl"
|
||||
alt="image"
|
||||
:style="{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
display: 'block',
|
||||
}"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="is-flex is-justify-content-center is-align-items-center"
|
||||
:style="{
|
||||
height: '100%',
|
||||
background: 'rgba(0, 0, 0, 0.3)',
|
||||
opacity: 0.15,
|
||||
}"
|
||||
>
|
||||
<SvgIcon
|
||||
v-bind="{
|
||||
name: 'image.svg',
|
||||
type: 'black',
|
||||
size: 180,
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr v-if="content.message">
|
||||
<td
|
||||
class="content-padding"
|
||||
:style="{
|
||||
paddingBottom: '20px',
|
||||
paddingTop: '30px',
|
||||
paddingRight: '25px',
|
||||
paddingLeft: '25px',
|
||||
}"
|
||||
>
|
||||
<div
|
||||
:style="{
|
||||
border: '1px solid #e5e7eb',
|
||||
borderRadius: '12px',
|
||||
backgroundColor: '#ffffff',
|
||||
overflow: 'hidden',
|
||||
}"
|
||||
>
|
||||
<div :style="{ padding: '15px' }">
|
||||
<div
|
||||
class="message-content"
|
||||
:style="{
|
||||
fontSize: '16px',
|
||||
margin: '0',
|
||||
color: '#374151',
|
||||
lineHeight: '1.6',
|
||||
wordBreak: 'break-word',
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
v-html="processedMessage"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="hasValidLinks"
|
||||
:style="{
|
||||
padding: '15px',
|
||||
paddingLeft: '35px',
|
||||
borderTop: '1px solid #e5e7eb',
|
||||
backgroundColor: '#f9fafb',
|
||||
}"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
v-for="(link, index) in validLinks"
|
||||
:key="index"
|
||||
:style="{ width: '100%', marginBottom: '8px' }"
|
||||
>
|
||||
<ul :style="{ paddingLeft: '20px', margin: 0 }">
|
||||
<li :style="{ listStyleType: 'disc' }">
|
||||
<a
|
||||
:style="{
|
||||
color: '#2563eb!important',
|
||||
fontWeight: '600',
|
||||
textDecoration: 'underline',
|
||||
display: 'inline-block',
|
||||
wordBreak: 'break-word',
|
||||
fontSize: '14px',
|
||||
}"
|
||||
target="_blank"
|
||||
:href="link"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{{ getLinkText(index) }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td
|
||||
class="company-padding"
|
||||
:style="{ paddingBottom: '20px' }"
|
||||
>
|
||||
<div
|
||||
class="company-info"
|
||||
:style="{
|
||||
background: 'linear-gradient(to right, #000000, #0f9b0f)',
|
||||
color: '#ffffff',
|
||||
borderRadius: '12px',
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="logo-container"
|
||||
:style="{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
marginBottom: '15px',
|
||||
backgroundColor: '#ffffff',
|
||||
borderRadius: '8px',
|
||||
overflow: 'hidden',
|
||||
}"
|
||||
>
|
||||
<img
|
||||
:src="imageUrl"
|
||||
alt="Utopia Footer"
|
||||
:style="{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
display: 'block',
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- <tr>
|
||||
<td
|
||||
:style="{
|
||||
backgroundColor: '#f9fafb',
|
||||
padding: '20px',
|
||||
borderTop: '1px solid #e5e7eb',
|
||||
textAlign: 'center',
|
||||
}"
|
||||
>
|
||||
<p
|
||||
:style="{
|
||||
color: '#9ca3af',
|
||||
fontSize: '11px',
|
||||
margin: '0',
|
||||
lineHeight: '1.5',
|
||||
}"
|
||||
>
|
||||
©2025 Utopia. Tất cả các quyền được bảo lưu.
|
||||
</p>
|
||||
</td>
|
||||
</tr> -->
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { imageUrl } from "~/components/marketing/email/Email.utils";
|
||||
import { computed } from "vue";
|
||||
|
||||
interface KeywordItem {
|
||||
keyword: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface Template1Content {
|
||||
subject?: string;
|
||||
message?: string;
|
||||
imageUrl?: string | null;
|
||||
linkUrl?: string[];
|
||||
textLinkUrl?: string[];
|
||||
keyword?: KeywordItem[];
|
||||
}
|
||||
|
||||
interface Props {
|
||||
content: Template1Content;
|
||||
previewMode?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
previewMode: false,
|
||||
});
|
||||
|
||||
const hasValidLinks = computed(() => {
|
||||
return props.content.linkUrl && props.content.linkUrl.some((link) => link && link.trim() !== "");
|
||||
});
|
||||
|
||||
const hasValidImage = computed(() => {
|
||||
return props.content.imageUrl && props.content.imageUrl.trim() !== "";
|
||||
});
|
||||
|
||||
const replaceKeywords = (content: string): string => {
|
||||
if (!content) return "";
|
||||
|
||||
let replacedContent = content;
|
||||
|
||||
if (props.content.keyword && Array.isArray(props.content.keyword)) {
|
||||
props.content.keyword.forEach((kw) => {
|
||||
if (typeof kw !== "string" && kw.keyword && kw.value) {
|
||||
const regex = new RegExp(`\\{\\{${kw.keyword}\\}\\}`, "g");
|
||||
replacedContent = replacedContent.replace(regex, kw.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return replacedContent;
|
||||
};
|
||||
|
||||
const processedMessage = computed(() => replaceKeywords(props.content.message || ""));
|
||||
|
||||
const validLinks = computed(() => {
|
||||
if (!props.content.linkUrl || props.content.linkUrl.length === 0) return [];
|
||||
return props.content.linkUrl.filter((link) => link && link.trim() !== "");
|
||||
});
|
||||
|
||||
const getLinkText = (index: number): string => {
|
||||
return props.content.textLinkUrl && props.content.textLinkUrl[index] && props.content.textLinkUrl[index].trim() !== ""
|
||||
? props.content.textLinkUrl[index]
|
||||
: `Link ${index + 1}`;
|
||||
};
|
||||
|
||||
const styles = `
|
||||
ol{
|
||||
list-style: decimal;
|
||||
}
|
||||
li{
|
||||
list-style: disc;
|
||||
}
|
||||
.greeting-padding{
|
||||
padding: 30px 25px 20px;
|
||||
}
|
||||
|
||||
.content-padding, .company-padding {
|
||||
padding: 0px 25px 20px;
|
||||
}
|
||||
|
||||
.message-content p {
|
||||
margin: 0 0 12px 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.message-content p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.message-content strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.message-content em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.message-content u {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.message-content s {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.message-content ol,
|
||||
.message-content ul {
|
||||
margin: 12px 0;
|
||||
padding-left: 25px;
|
||||
}
|
||||
|
||||
.message-content ol li,
|
||||
.message-content ul li {
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.message-content a {
|
||||
color: #2563eb;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.message-content h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
margin: 20px 0 12px 0;
|
||||
line-height: 1.3;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.message-content h2 {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
margin: 18px 0 12px 0;
|
||||
line-height: 1.3;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.message-content h3 {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
margin: 16px 0 12px 0;
|
||||
line-height: 1.3;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.message-content .ql-align-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.message-content .ql-align-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.message-content .ql-align-justify {
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.message-content .ql-indent-1 {
|
||||
padding-left: 3em;
|
||||
}
|
||||
|
||||
.message-content .ql-indent-2 {
|
||||
padding-left: 6em;
|
||||
}
|
||||
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
|
||||
.email-container {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.greeting-padding{
|
||||
padding: 20px 0 0 0 !important;
|
||||
}
|
||||
|
||||
.content-padding {
|
||||
padding: 20px 0px !important;
|
||||
}
|
||||
.company-padding {
|
||||
padding: 0px 0px 20px 0px !important;
|
||||
}
|
||||
.company-info {
|
||||
padding: 20px 10px !important;
|
||||
}
|
||||
.greeting-title{
|
||||
padding: 10px !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.header-image {
|
||||
width: 100% !important;
|
||||
height: 250px !important;
|
||||
}
|
||||
.greeting-text {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
.message-text {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
.logo-container {
|
||||
width: 150px !important;
|
||||
height: 45px !important;
|
||||
}
|
||||
.company-name {
|
||||
font-size: 12px !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
.contact-info {
|
||||
font-size: 11px !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
</script>
|
||||
@@ -248,26 +248,19 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useNuxtApp } from "nuxt/app";
|
||||
import { nextTick } from "vue";
|
||||
import { render } from "@vue-email/render";
|
||||
const nuxtApp = useNuxtApp();
|
||||
import Modal from "~/components/Modal.vue";
|
||||
import Template1 from "~/components/marketing/email/Template1.vue";
|
||||
import SvgIcon from "~/components/SvgIcon.vue";
|
||||
import Editor from "~/components/common/Editor.vue";
|
||||
|
||||
const nuxtApp = useNuxtApp();
|
||||
const $snackbar = nuxtApp.$snackbar as (message?: string) => void;
|
||||
const $getpath = nuxtApp.$getpath as (message?: string) => void;
|
||||
const $getEditRights = nuxtApp.$getEditRights as () => boolean;
|
||||
|
||||
// const { $getpath, $snackbar} = useNuxtApp()
|
||||
|
||||
import { ref, watch, nextTick } from "vue";
|
||||
|
||||
import axios from "axios";
|
||||
import Modal from "~/components/Modal.vue";
|
||||
import Template1 from "~/lib/email/templates/Template1.vue";
|
||||
import SvgIcon from "~/components/SvgIcon.vue";
|
||||
import Editor from "~/components/common/Editor.vue";
|
||||
|
||||
const apiUrl = `${$getpath()}data`;
|
||||
const sendEmailUrl = `${$getpath()}send-email`;
|
||||
const showmodal = ref();
|
||||
|
||||
// Types
|
||||
@@ -566,11 +559,7 @@ const handleSendEmail = async () => {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error sending email:", error);
|
||||
const errorMsg = axios.isAxiosError(error)
|
||||
? error.response?.data?.error || error.message
|
||||
: "Unknown error occurred";
|
||||
|
||||
$snackbar(`Failed to send email: ${errorMsg}`);
|
||||
$snackbar(`Failed to send email: ${error.message}`);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -618,15 +607,10 @@ const getData = async () => {
|
||||
const emailListUrl = `${apiUrl}/Email_List/`;
|
||||
|
||||
try {
|
||||
const response = await axios.get(emailListUrl);
|
||||
if (response.status === 200) {
|
||||
console.log(response);
|
||||
dataEmails.value = response.data.rows;
|
||||
|
||||
console.log(dataEmails.value);
|
||||
} else {
|
||||
throw new Error("Failed to fetch email list");
|
||||
}
|
||||
const response = await $fetch(emailListUrl);
|
||||
dataEmails.value = response.rows;
|
||||
console.log(response);
|
||||
console.log(dataEmails.value);
|
||||
} catch (error) {
|
||||
console.error("Error fetching email list:", error);
|
||||
throw error;
|
||||
|
||||
@@ -1,287 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="templateEmailContent"
|
||||
class="view-email"
|
||||
>
|
||||
<Template1
|
||||
v-bind="templateProps"
|
||||
previewMode
|
||||
/>
|
||||
|
||||
<div class="action mt-3">
|
||||
<button
|
||||
class="button is-info mx-3 has-text-white"
|
||||
:class="{ 'is-loading': isLoading || workflowIsLoading }"
|
||||
@click="handleSendEmail()"
|
||||
>
|
||||
<span>Gửi mail</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>Chưa có nội dung email</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, computed } from "vue";
|
||||
import { render } from "@vue-email/render";
|
||||
|
||||
import Template1 from "~/lib/email/templates/Template1.vue";
|
||||
|
||||
const {
|
||||
$insertapi,
|
||||
$snackbar,
|
||||
$getdata,
|
||||
$mode,
|
||||
$findapi,
|
||||
$getapi,
|
||||
$numtoString,
|
||||
$numberToVietnamese,
|
||||
$numberToVietnameseCurrency,
|
||||
$formatDateVN,
|
||||
$getFirstAndLastName,
|
||||
$store,
|
||||
$paymentQR,
|
||||
} = useNuxtApp();
|
||||
const isLoading = ref(false);
|
||||
|
||||
const templateEmailContent = ref(null);
|
||||
const props = defineProps({
|
||||
data: Object,
|
||||
idEmailTemplate: Number,
|
||||
scheduleItemId: Number,
|
||||
});
|
||||
|
||||
const isVietnamese = computed(() => $store.lang.toLowerCase() === "vi");
|
||||
|
||||
const paymentScheduleItem = ref(null);
|
||||
const contentPaymentQR = ref("");
|
||||
|
||||
const emailTemplate = await $getdata("emailtemplate", { filter: { id: props.idEmailTemplate }, first: true });
|
||||
|
||||
templateEmailContent.value = emailTemplate[0] ?? null;
|
||||
|
||||
let foundPaymentSchedule = $findapi("payment_schedule");
|
||||
foundPaymentSchedule.params = {
|
||||
values:
|
||||
"id,code,from_date,to_date,amount,cycle,paid_amount,remain_amount,txn_detail,txn_detail__transaction,txn_detail__transaction__customer,txn_detail__transaction__customer__type__code,txn_detail__transaction__customer__code,txn_detail__transaction__customer__fullname,txn_detail__transaction__customer__phone,txn_detail__transaction__customer__email,txn_detail__transaction__customer__contact_address,txn_detail__transaction__customer__address,txn_detail__transaction__customer__legal_code,txn_detail__transaction__product__trade_code",
|
||||
filter: { id: props.scheduleItemId },
|
||||
};
|
||||
|
||||
async function paymentSchedule() {
|
||||
try {
|
||||
const [paymentScheduleRes] = await $getapi([foundPaymentSchedule]);
|
||||
|
||||
paymentScheduleItem.value = paymentScheduleRes?.data?.rows[0] || null;
|
||||
contentPaymentQR.value = buildContentPayment(paymentScheduleItem.value);
|
||||
} catch (error) {
|
||||
if ($mode === "dev") {
|
||||
console.error("Call api product error", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
paymentSchedule();
|
||||
|
||||
const buildQrHtml = (url) => `
|
||||
<div style="text-align: center; margin-top: 16px">
|
||||
<img
|
||||
src="${url}"
|
||||
alt="VietQR"
|
||||
width="500"
|
||||
style="display: inline-block"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const buildContentPayment = (data) => {
|
||||
const {
|
||||
txn_detail__transaction__customer__type__code: customerType,
|
||||
txn_detail__transaction__customer__code: customerCode,
|
||||
txn_detail__transaction__customer__fullname: customerName,
|
||||
txn_detail__transaction__product__trade_code: productCode,
|
||||
cycle,
|
||||
} = data;
|
||||
|
||||
if (customerType.toLowerCase() === "cn") {
|
||||
if (customerName.length < 14) {
|
||||
return `${productCode} ${customerCode} ${customerName} TT ${cycle == 0 ? "Dat Coc" : `Dot ${cycle}`}`;
|
||||
} else {
|
||||
return `${productCode} ${customerCode} ${$getFirstAndLastName(customerName)} TT ${cycle == 0 ? "Dat Coc" : `Dot ${cycle}`}`;
|
||||
}
|
||||
} else {
|
||||
return `${productCode} ${customerCode} TT ${cycle == 0 ? "Dat Coc" : `Dot ${cycle}`}`;
|
||||
}
|
||||
};
|
||||
|
||||
const templateProps = computed(() => {
|
||||
let content = templateEmailContent.value.content.content || "";
|
||||
|
||||
// 1️⃣ XÓA TOÀN BỘ QR CŨ
|
||||
content = content.replace(/<div[^>]*>\s*<img[^>]*img\.vietqr\.io[^>]*>\s*<\/div>/g, "");
|
||||
|
||||
// 2️⃣ CHỈ APPEND 1 QR CUỐI CÙNG (NẾU CÓ URL)
|
||||
if ($paymentQR(contentPaymentQR.value)) {
|
||||
content = `
|
||||
${content.trim()}
|
||||
|
||||
${buildQrHtml($paymentQR(contentPaymentQR.value))}
|
||||
`;
|
||||
}
|
||||
|
||||
return {
|
||||
content: {
|
||||
subject: templateEmailContent.value.content.subject || "Thông báo mới",
|
||||
message: replaceTemplateVars(content) || "Bạn có một thông báo mới.",
|
||||
imageUrl: templateEmailContent.value.content.imageUrl || null,
|
||||
linkUrl: templateEmailContent.value.content.linkUrl || [""],
|
||||
textLinkUrl: templateEmailContent.value.content.textLinkUrl || [""],
|
||||
keyword: Array.isArray(templateEmailContent.value.content.keyword)
|
||||
? templateEmailContent.value.content.keyword.map((k) => (typeof k === "string" ? { keyword: k, value: "" } : k))
|
||||
: [{ keyword: "", value: "" }],
|
||||
},
|
||||
previewMode: true,
|
||||
};
|
||||
});
|
||||
|
||||
const handleSendEmail = async () => {
|
||||
isLoading.value = true;
|
||||
const tempEm = {
|
||||
value: {
|
||||
...templateProps.value,
|
||||
content: {
|
||||
...templateProps.value.content,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// ===== QUILL → HTML EMAIL (INLINE STYLE) =====
|
||||
tempEm.value.content.message = quillToEmailHtml(templateProps.value.content.message);
|
||||
let emailHtml = await render(Template1, tempEm.value);
|
||||
|
||||
// If no image URL provided, remove image section from HTML
|
||||
if ((templateProps.value.content.imageUrl ?? "").trim() === "") {
|
||||
emailHtml = emailHtml.replace(/<tr\s+class=["']header-row["'][^>]*>[\s\S]*?<\/tr>/gi, "");
|
||||
emailHtml = emailHtml.replace(/\n\s*\n\s*\n/g, "\n\n");
|
||||
}
|
||||
|
||||
// Replace keywords in HTML
|
||||
let finalEmailHtml = emailHtml;
|
||||
if (templateProps.value.content.keyword && templateProps.value.content.keyword.length > 0) {
|
||||
templateProps.value.content.keyword.forEach(({ keyword, value }) => {
|
||||
if (keyword && value) {
|
||||
const regex = new RegExp(`\\{\\{${keyword}\\}\\}`, "g");
|
||||
finalEmailHtml = finalEmailHtml.replace(regex, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const response = await $insertapi("sendemail", {
|
||||
data: {
|
||||
to: paymentScheduleItem.value?.txn_detail__transaction__customer__email,
|
||||
content: finalEmailHtml,
|
||||
subject: replaceTemplateVars(templateProps.value.content.subject) || "Thông báo từ Utopia Villas & Resort",
|
||||
},
|
||||
notify: false,
|
||||
});
|
||||
if (response !== null) {
|
||||
isLoading.value = false;
|
||||
$snackbar(
|
||||
isVietnamese
|
||||
? `Thông báo đã được gửi thành công đến khách hàng.`
|
||||
: `The notification has been successfully sent to the customer.`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
function replaceTemplateVars(html) {
|
||||
return html
|
||||
.replace(/\[day]/g, String(new Date().getDate()).padStart(2, "0") || "")
|
||||
.replace(/\[month]/g, String(new Date().getMonth() + 1).padStart(2, "0") || "")
|
||||
.replace(/\[year]/g, new Date().getFullYear() || "")
|
||||
.replace(/\[product\.trade_code\]/g, paymentScheduleItem.value?.txn_detail__transaction__product__trade_code)
|
||||
.replace(
|
||||
/\[product\.trade_code_payment\]/g,
|
||||
sanitizeContentPayment(paymentScheduleItem.value?.txn_detail__transaction__product__trade_code),
|
||||
)
|
||||
.replace(/\[customer\.fullname\]/g, paymentScheduleItem.value?.txn_detail__transaction__customer__fullname)
|
||||
.replace(
|
||||
/\[customer\.name\]/g,
|
||||
`${paymentScheduleItem.value?.txn_detail__transaction__customer__type__code.toLowerCase() == "cn" ? (paymentScheduleItem.value?.txn_detail__transaction__customer__fullname.length < 14 ? paymentScheduleItem.value?.txn_detail__transaction__customer__fullname : $getFirstAndLastName(paymentScheduleItem.value.txn_detail__transaction__customer__fullname)) : ""}` ||
|
||||
"",
|
||||
)
|
||||
.replace(/\[customer\.code\]/g, paymentScheduleItem.value?.txn_detail__transaction__customer__code || "")
|
||||
.replace(
|
||||
/\[customer\.legal_code\]/g,
|
||||
paymentScheduleItem.value?.txn_detail__transaction__customer__legal_code || "",
|
||||
)
|
||||
.replace(
|
||||
/\[customer\.contact_address\]/g,
|
||||
paymentScheduleItem.value?.txn_detail__transaction__customer__contact_address ||
|
||||
paymentScheduleItem.value?.txn_detail__transaction__customer__address ||
|
||||
"",
|
||||
)
|
||||
.replace(/\[customer\.phone\]/g, paymentScheduleItem.value?.txn_detail__transaction__customer__phone || "")
|
||||
.replace(/\[payment_schedule\.from_date\]/g, $formatDateVN(paymentScheduleItem.value?.from_date) || "")
|
||||
.replace(/\[payment_schedule\.to_date\]/g, $formatDateVN(paymentScheduleItem.value?.to_date) || "")
|
||||
.replace(/\[payment_schedule\.amount\]/g, $numtoString(paymentScheduleItem.value?.remain_amount) || "")
|
||||
.replace(
|
||||
/\[payment_schedule\.amount_in_word\]/g,
|
||||
$numberToVietnameseCurrency(paymentScheduleItem.value?.remain_amount) || "",
|
||||
)
|
||||
.replace(/\[payment_schedule\.cycle\]/g, $numtoString(paymentScheduleItem.value?.cycle) || "")
|
||||
.replace(
|
||||
/\[payment_schedule\.cycle-in-words\]/g,
|
||||
`${paymentScheduleItem.value?.cycle == 0 ? "đặt cọc" : `đợt thứ ${$numberToVietnamese(paymentScheduleItem.value?.cycle).toLowerCase()}`}` ||
|
||||
"",
|
||||
)
|
||||
.replace(
|
||||
/\[payment_schedule\.note\]/g,
|
||||
`${paymentScheduleItem.value?.cycle == 0 ? "Dat coc" : `Dot ${paymentScheduleItem.value?.cycle}`}` || "",
|
||||
);
|
||||
}
|
||||
|
||||
function quillToEmailHtml(html) {
|
||||
return (
|
||||
html
|
||||
// ALIGN
|
||||
.replace(/class="([^"]*?)ql-align-center([^"]*?)"/g, 'style="text-align:center;"')
|
||||
.replace(/class="([^"]*?)ql-align-right([^"]*?)"/g, 'style="text-align:right;"')
|
||||
.replace(/class="([^"]*?)ql-align-left([^"]*?)"/g, 'style="text-align:left;"')
|
||||
.replace(/class="([^"]*?)ql-align-justify([^"]*?)"/g, 'style="text-align:justify;"')
|
||||
|
||||
// FONT SIZE
|
||||
.replace(/ql-size-small/g, "")
|
||||
.replace(/ql-size-large/g, "")
|
||||
.replace(/ql-size-huge/g, "")
|
||||
|
||||
// REMOVE EMPTY CLASS
|
||||
.replace(/class=""/g, "")
|
||||
);
|
||||
}
|
||||
|
||||
function sanitizeContentPayment(text, maxLength = 80) {
|
||||
if (!text) return "";
|
||||
|
||||
return text
|
||||
.normalize("NFD") // bỏ dấu tiếng Việt
|
||||
.replace(/[\u0300-\u036f]/g, "")
|
||||
.replace(/[^a-zA-Z0-9 _-]/g, "") // bỏ ký tự lạ
|
||||
.replace(/\s+/g, " ")
|
||||
.trim()
|
||||
.slice(0, maxLength);
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.view-email .view-email-wrapper {
|
||||
border: 1px solid #ccc;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
background-color: #f9f9f9;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
width: 80%;
|
||||
overflow-y: auto;
|
||||
margin-bottom: 15px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.action {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user