Initial commit

This commit is contained in:
Viet An
2026-03-02 09:45:33 +07:00
commit d17a9e2588
415 changed files with 92113 additions and 0 deletions

View File

@@ -0,0 +1,283 @@
<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,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',
{
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\.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>