320 lines
11 KiB
JavaScript
320 lines
11 KiB
JavaScript
import { render } from '@vue-email/render';
|
|
import Template1 from '@/lib/email/templates/Template1.vue';
|
|
import { forEachAsync } from 'es-toolkit';
|
|
|
|
export default function useSendEmail(filter, templateId) {
|
|
const paymentSchedules = ref(null);
|
|
const contents = ref(null);
|
|
const isSending = ref(false);
|
|
const emailTemplate = ref(null);
|
|
|
|
const {
|
|
$dayjs,
|
|
$getdata,
|
|
$insertapi,
|
|
$numtoString,
|
|
$numberToVietnamese,
|
|
$numberToVietnameseCurrency,
|
|
$formatDateVN,
|
|
$getFirstAndLastName,
|
|
$snackbar,
|
|
$store,
|
|
$paymentQR
|
|
} = useNuxtApp();
|
|
|
|
const buildQrHtml = (url) => `
|
|
<div style="text-align: center; margin-top: 16px">
|
|
<img
|
|
src="${url}"
|
|
alt="VietQR"
|
|
width="500"
|
|
style="display: inline-block"
|
|
/>
|
|
</div>
|
|
`;
|
|
|
|
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 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}`}`;
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
function addDays(days) {
|
|
const today = new Date();
|
|
today.setDate(today.getDate() + days);
|
|
return today;
|
|
}
|
|
|
|
function replaceTemplateVars(html, paymentScheduleItem) {
|
|
const {
|
|
txn_detail__transaction__product__trade_code,
|
|
txn_detail__transaction__customer__code,
|
|
txn_detail__transaction__customer__fullname,
|
|
txn_detail__transaction__customer__legal_code,
|
|
txn_detail__transaction__customer__type__code,
|
|
txn_detail__transaction__customer__contact_address,
|
|
txn_detail__transaction__customer__address,
|
|
txn_detail__transaction__customer__phone,
|
|
txn_detail__transaction__date,
|
|
from_date,
|
|
to_date,
|
|
cycle,
|
|
amount,
|
|
paid_amount,
|
|
penalty_amount,
|
|
remain_amount,
|
|
} = paymentScheduleItem;
|
|
|
|
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, txn_detail__transaction__product__trade_code)
|
|
.replace(/\[product\.trade_code_payment\]/g, sanitizeContentPayment(txn_detail__transaction__product__trade_code))
|
|
.replace(/\[customer\.fullname\]/g, txn_detail__transaction__customer__fullname)
|
|
.replace(
|
|
/\[customer\.name\]/g,
|
|
`${txn_detail__transaction__customer__type__code.toLowerCase() == 'cn' ? (txn_detail__transaction__customer__fullname.length < 14 ? txn_detail__transaction__customer__fullname : $getFirstAndLastName(txn_detail__transaction__customer__fullname)) : ''}` ||
|
|
'',
|
|
)
|
|
.replace(/\[customer\.code\]/g, txn_detail__transaction__customer__code || '')
|
|
.replace(
|
|
/\[customer\.legal_code\]/g,
|
|
txn_detail__transaction__customer__legal_code || '',
|
|
)
|
|
.replace(
|
|
/\[customer\.contact_address\]/g,
|
|
txn_detail__transaction__customer__contact_address ||
|
|
txn_detail__transaction__customer__address ||
|
|
'',
|
|
)
|
|
.replace(/\[customer\.phone\]/g, txn_detail__transaction__customer__phone || '')
|
|
.replace(/\[payment_schedule\.from_date\]/g, $formatDateVN(from_date) || '')
|
|
.replace(/\[payment_schedule\.to_date\]/g, $formatDateVN(to_date) || '')
|
|
.replace(/\[payment_schedule\.expiration_date\]/g, $formatDateVN(addDays(3)) || '')
|
|
.replace(/\[payment_schedule\.amount_remain\]/g, $numtoString(amount) || '')
|
|
.replace(/\[payment_schedule\.paid_amount\]/g, $numtoString(paid_amount) || '')
|
|
.replace(/\[payment_schedule\.penalty_amount\]/g, $numtoString(penalty_amount) || '')
|
|
.replace(/\[payment_schedule\.amount\]/g, $numtoString(remain_amount) || '')
|
|
.replace(
|
|
/\[payment_schedule\.amount_in_word\]/g,
|
|
$numberToVietnameseCurrency(remain_amount) || '',
|
|
)
|
|
.replace(/\[payment_schedule\.cycle\]/g, $numtoString(cycle) || '')
|
|
.replace(
|
|
/\[payment_schedule\.cycle-in-words\]/g,
|
|
`${cycle == 0 ? 'đặt cọc' : `đợt thứ ${$numberToVietnamese(cycle).toLowerCase()}`}` ||
|
|
'',
|
|
)
|
|
.replace(
|
|
/\[payment_schedule\.note\]/g,
|
|
`${cycle == 0 ? 'Dat coc' : `Dot ${cycle}`}` || '',
|
|
)
|
|
.replace(
|
|
/\[transaction\.date\]/g, formatDateVNText(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 buildContent(paymentSchedule) {
|
|
let message = emailTemplate.value.content.content;
|
|
message = message.replace(
|
|
/<div[^>]*>\s*<img[^>]*img\.vietqr\.io[^>]*>\s*<\/div>/g,
|
|
'',
|
|
);
|
|
|
|
const qr = buildQrHtml($paymentQR(buildContentPayment(paymentSchedule), 'TCB'));
|
|
message = message.trim() + qr;
|
|
|
|
return {
|
|
...emailTemplate.value.content,
|
|
content: undefined,
|
|
message: replaceTemplateVars(message, paymentSchedule),
|
|
};
|
|
}
|
|
|
|
async function send() {
|
|
isSending.value = true;
|
|
$snackbar(`Đang gửi ${contents.value.length} email thông báo...`);
|
|
|
|
await forEachAsync(contents.value, async (bigContent, i) => {
|
|
const { imageUrl, keyword, message, subject } = bigContent;
|
|
const tempEm = {
|
|
content: toRaw(bigContent),
|
|
previewMode: true,
|
|
};
|
|
|
|
// ===== QUILL → HTML EMAIL (INLINE STYLE) =====
|
|
tempEm.content.message = quillToEmailHtml(message);
|
|
let emailHtml = await render(Template1, tempEm);
|
|
|
|
// If no image URL provided, remove image section from HTML
|
|
if ((imageUrl ?? '').trim() === '') {
|
|
emailHtml = emailHtml
|
|
.replace(/<tr\s+class=["']header-row["'][^>]*>[\s\S]*?<\/tr>/gi, '')
|
|
.replace(/\n\s*\n\s*\n/g, '\n\n');
|
|
}
|
|
|
|
// Replace keywords in HTML
|
|
let finalEmailHtml = emailHtml;
|
|
if (keyword && keyword.length > 0) {
|
|
keyword.forEach(({ keyword, value }) => {
|
|
if (keyword && value) {
|
|
const regex = new RegExp(`\\{\\{${keyword}\\}\\}`, 'g');
|
|
finalEmailHtml = finalEmailHtml.replace(regex, value);
|
|
}
|
|
});
|
|
}
|
|
|
|
const response = await $insertapi(
|
|
'sendemail',
|
|
{
|
|
to: paymentSchedules.value[i].txn_detail__transaction__customer__email,
|
|
content: finalEmailHtml,
|
|
subject: replaceTemplateVars(subject, paymentSchedules.value[i]) || 'Thông báo từ Utopia Villas & Resort',
|
|
},
|
|
undefined,
|
|
false,
|
|
);
|
|
|
|
if (response !== null) {
|
|
await $insertapi('productnote', {
|
|
ref: paymentSchedules.value[i].txn_detail__transaction__product,
|
|
user: $store.login.id,
|
|
detail: `Đã gửi email thông báo nhắc nợ cho sản phẩm ${paymentSchedules.value[i].txn_detail__transaction__product__trade_code} vào lúc ${$dayjs().format('HH:mm ngày DD/MM/YYYY')}.`
|
|
}, undefined, false);
|
|
}
|
|
});
|
|
|
|
setTimeout(() => {
|
|
$snackbar('Thông báo đã được gửi thành công đến khách hàng.');
|
|
isSending.value = false;
|
|
}, 1000);
|
|
}
|
|
|
|
async function populatePaymentSchedules(idFilters = {}) {
|
|
const paymentScheduleData = await $getdata(
|
|
'payment_schedule',
|
|
undefined,
|
|
{
|
|
filter: {
|
|
...filter.value,
|
|
...idFilters,
|
|
},
|
|
sort: 'to_date',
|
|
values: 'id,penalty_paid,penalty_remain,penalty_amount,penalty_reduce,batch_date,amount_remain,paid_amount,remain_amount,code,status,txn_detail,txn_detail__transaction__date,txn_detail__transaction__product,txn_detail__transaction__product__trade_code,txn_detail__transaction__code,txn_detail__code,txn_detail__transaction__customer__code,txn_detail__transaction__customer__fullname,txn_detail__transaction__customer__email,txn_detail__transaction__customer__type__code,txn_detail__transaction__customer__legal_code,txn_detail__transaction__customer__contact_address,txn_detail__transaction__customer__address,txn_detail__transaction__customer__phone,txn_detail__transaction__policy__code,txn_detail__phase__name,type__name,from_date,to_date,amount,cycle,cycle_days,status__name,detail,entry',
|
|
}
|
|
);
|
|
|
|
paymentSchedules.value = paymentScheduleData;
|
|
contents.value = paymentSchedules.value.map(buildContent);
|
|
}
|
|
|
|
async function populateEmailTemplate() {
|
|
const emailTemplateData = await $getdata(
|
|
'emailtemplate',
|
|
{ id: templateId },
|
|
undefined,
|
|
true,
|
|
);
|
|
|
|
emailTemplate.value = emailTemplateData;
|
|
}
|
|
|
|
onMounted(async () => {
|
|
await populateEmailTemplate();
|
|
populatePaymentSchedules();
|
|
});
|
|
|
|
watch(filter, () => {
|
|
populatePaymentSchedules();
|
|
}, { deep: true });
|
|
|
|
const storeProp = `selectedPaymentSchedulesForEmailIn${templateId === 13 ? 'Due' : 'Overdue'}`;
|
|
|
|
watch(() => $store[storeProp], (val) => {
|
|
populatePaymentSchedules({ id__in: val });
|
|
})
|
|
|
|
return { contents, send, isSending };
|
|
} |