Files
hrm/app/components/marketing/email/viewEmail/ViewEmail.vue
2026-04-06 15:53:14 +07:00

338 lines
12 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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 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', { id: props.idEmailTemplate }, undefined, false);
templateEmailContent.value = emailTemplate[0] ?? null;
let foundPaymentSchedule = $findapi('payment_schedule');
foundPaymentSchedule.params = {
values:
'id,code,from_date,to_date,amount,cycle,paid_amount,amount_remain,remain_amount,penalty_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,txn_detail__transaction__date',
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',
{
to: paymentScheduleItem.value?.txn_detail__transaction__customer__email,
content: finalEmailHtml,
subject: replaceTemplateVars(templateProps.value.content.subject) || 'Thông báo từ Utopia Villas & Resort',
},
undefined,
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\.expiration_date\]/g, $formatDateVN(addDays(3)) || '')
.replace(/\[payment_schedule\.amount_remain\]/g, $numtoString(paymentScheduleItem.value?.amount) || '')
.replace(/\[payment_schedule\.paid_amount\]/g, $numtoString(paymentScheduleItem.value?.paid_amount) || '')
.replace(/\[payment_schedule\.penalty_amount\]/g, $numtoString(paymentScheduleItem.value?.penalty_amount) || '')
.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}`}` || '',
)
.replace(
/\[transaction\.date\]/g, formatDateVNText(paymentScheduleItem.value?.txn_detail__transaction__date) || '',
);
}
function quillToEmailHtml(html) {
return html
// ALIGN (chỉ replace align class, không phá class khác)
.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 (xóa class size)
.replace(/ql-size-small/g, '')
.replace(/ql-size-large/g, '')
.replace(/ql-size-huge/g, '')
// REMOVE EMPTY CLASS
.replace(/class=""/g, '')
// FIX FIGURE MARGIN LEFT + RIGHT
.replace(
/<figure([^>]*?)style="([^"]*)"/g,
(match, before, styles) => {
if (!/margin-left\s*:/.test(styles)) {
styles += ';margin-left:0';
}
if (!/margin-right\s*:/.test(styles)) {
styles += ';margin-right:0';
}
return `<figure${before}style="${styles}"`;
}
);
}
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);
}
const formatDateVNText = (input, { withTime = false, withSeconds = false } = {}) => {
if (!input) return '';
const date = new Date(input);
if (isNaN(date.getTime())) return '';
const pad = (n) => String(n).padStart(2, '0');
const day = pad(date.getDate());
const month = pad(date.getMonth() + 1);
const year = date.getFullYear();
let result = `${day} tháng ${month} năm ${year}`;
if (withTime) {
const hours = pad(date.getHours());
const minutes = pad(date.getMinutes());
result += ` ${hours}:${minutes}`;
if (withSeconds) {
const seconds = pad(date.getSeconds());
result += `:${seconds}`;
}
}
return result;
};
function addDays(days) {
const today = new Date();
today.setDate(today.getDate() + days);
return today;
}
</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>