import { render } from '@vue-email/render'; import Template1 from '@/lib/email/templates/Template1.vue'; import { forEachAsync, groupBy, last } from 'es-toolkit'; export default function useSendHandoverEmail(filter, templateName) { const products = ref(null); const contents = ref(null); const isSending = ref(false); const emailTemplate = ref(null); const templateNameRef = toRef(templateName); watch(templateNameRef, async () => { await populateEmailTemplate(); populateProducts(); }) const { $dayjs, $getdata, $insertapi, $numtoString, $numberToVietnamese, $numberToVietnameseCurrency, $formatDateVN, $paymentQR, $getFirstAndLastName, $snackbar, $store, } = useNuxtApp(); const buildQrHtml = (url) => `
VietQR
`; 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(product) { const { trade_code, prdbk__transaction__customer__type__code: customerType, prdbk__transaction__customer__code: customerCode, prdbk__transaction__customer__fullname: customerName, prdbk__transaction__resvtxn__psh__cycle: cycle, } = product; if (customerType?.toLowerCase() === 'cn') { if (customerName.length < 14) { return `${trade_code} ${customerCode} ${customerName} TT ${cycle == 0 ? 'Dat Coc' : `Dot ${cycle}`}`; } else { return `${trade_code} ${customerCode} ${$getFirstAndLastName(customerName)} TT ${cycle == 0 ? 'Dat Coc' : `Dot ${cycle}`}`; } } else return `${trade_code} ${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, product) { const { trade_code, number_of_floors, elpro__current_area, prdbk__transaction__date, prdbk__transaction__customer__code: cusCode, prdbk__transaction__customer__fullname: cusName, prdbk__transaction__customer__legal_code: cusLegalCode, prdbk__transaction__customer__issued_date: cusIssuedDate, prdbk__transaction__customer__type__code: cusTypeCode, prdbk__transaction__customer__contact_address: cusContactAddress, prdbk__transaction__customer__address: cusAddress, prdbk__transaction__customer__phone: cusPhone, prdbk__transaction__customer__email: cusEmail, prdbk__transaction__customer__orgncust__bank_account: orgBankAccount, prdbk__transaction__customer__orgncust__bank_name: orgBankName, prdbk__transaction__customer__orgncust__orgrep__people__fullname: orgPeopleFullname, prdbk__transaction__customer__orgncust__orgrep__relation__name: orgRelationName, prdbk__transaction__co_op__people__fullname: coopFullname, prdbk__transaction__co_op__people__legal_code: coopLegalCode, prdbk__transaction__co_op__people__address: coopAddress, prdbk__transaction__co_op__people__contact_address: coopContactAddress, prdbk__transaction__co_op__people__phone: coopPhone, prdbk__transaction__co_op__people__email: coopEmail, prdbk__transaction__resvtxn__psh__amount: amount, prdbk__transaction__resvtxn__psh__paid_amount: paid_amount, prdbk__transaction__resvtxn__psh__penalty_remain: penalty_remain, prdbk__transaction__resvtxn__psh__remain_amount: remain_amount, prdbk__transaction__resvtxn__psh__cycle: cycle, prdbk__transaction__resvtxn__psh__to_date: to_date, } = product; const getName = () => { if (cusTypeCode === 'CN') { return cusName.length < 14 ? cusName : $getFirstAndLastName(cusName); } return ''; } const issuedDate = $dayjs(cusIssuedDate); const issued_date_d = issuedDate.date(); const issued_date_m = issuedDate.month() + 1; const issued_date_y = issuedDate.year(); let replaced = html .replaceAll('[day]', String(new Date().getDate()).padStart(2, '0')) .replaceAll('[month]', String(new Date().getMonth() + 1).padStart(2, '0')) .replaceAll('[year]', new Date().getFullYear()) .replaceAll('[product.trade_code]', trade_code) .replaceAll('[product.elpro__current_area]', elpro__current_area) .replaceAll('[product.elpro__current_area_in_words]', $numberToVietnameseCurrency(elpro__current_area)) .replaceAll('[product.number_of_floors]', number_of_floors) .replaceAll('[customer.name]', cusName) .replaceAll('[customer.code]', cusCode) .replaceAll('[customer.fullname]', cusName) .replaceAll('[customer.legal_code]', cusLegalCode) .replaceAll('[customer.address]', cusAddress) .replaceAll('[customer.contact_address]', cusContactAddress || cusAddress || '') .replaceAll('[customer.phone]', cusPhone) .replaceAll('[customer.email]', cusEmail) .replaceAll('[issued_date_d]', issued_date_d) .replaceAll('[issued_date_m]', issued_date_m) .replaceAll('[issued_date_y]', issued_date_y) .replaceAll('[customer.orgncust.bank_account]', orgBankAccount) .replaceAll('[customer.orgncust.bank_name]', orgBankName) .replaceAll('[customer.orgncust.orgrep.people.fullname]', orgPeopleFullname) .replaceAll('[customer.orgncust.orgrep.relation.name]', orgRelationName) .replaceAll('[co_op.people.fullname]', coopFullname) .replaceAll('[co_op.people.legal_code]', coopLegalCode) .replaceAll('[co_op.people.address]', coopAddress) .replaceAll('[co_op.people.contact_address]', coopContactAddress || coopAddress || '') .replaceAll('[co_op.people.phone]', coopPhone || '') .replaceAll('[co_op.people.email]', coopEmail || '') if (templateNameRef.value !== 'Biên bản bàn giao đất - Cá nhân') { replaced = replaced .replaceAll('[product.trade_code_payment]', sanitizeContentPayment(trade_code)) .replaceAll('[transaction.date]', formatDateVNText(prdbk__transaction__date) || '') .replaceAll('[customer.name]', getName()) .replaceAll('[payment_schedule.remain_amount]', $numtoString((remain_amount).toFixed()) || '') .replaceAll('[payment_schedule.paid_amount]', $numtoString(paid_amount.toFixed()) || '') .replaceAll('[payment_schedule.penalty_remain]', $numtoString(penalty_remain.toFixed()) || '') .replaceAll('[payment_schedule.amount]', $numtoString(amount) || '') .replaceAll('[payment_schedule.remain_amount_in_word]', $numberToVietnameseCurrency(remain_amount) || '') .replaceAll('[payment_schedule.to_date]', $formatDateVN(to_date) || '') .replaceAll('[payment_schedule.expiration_date]', $formatDateVN(addDays(3)) || '') .replaceAll('[payment_schedule.cycle]', $numtoString(cycle) || '') .replaceAll( '[payment_schedule.cycle-in-words]', cycle == 0 ? 'đặt cọc' : `đợt thứ ${$numberToVietnamese(cycle).toLowerCase()}`, ) .replaceAll('[payment_schedule.note]', cycle == 0 ? 'đặt cọc' : `đợt ${cycle}`) } return replaced; } 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( /]*?)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 ` { const { imageUrl, keyword } = bigContent; const tempEm = { content: toRaw(bigContent), previewMode: true, }; // ===== QUILL → HTML EMAIL (INLINE STYLE) ===== let emailHtml = await render(Template1, tempEm); // If no image URL provided, remove image section from HTML if ((imageUrl ?? '').trim() === '') { emailHtml = emailHtml .replace(/]*>[\s\S]*?<\/tr>/gi, '') .replace(/\n\s*\n\s*\n/g, '\n\n'); } // Replace keywords in HTML if (keyword && keyword.length > 0) { keyword.forEach(({ keyword, value }) => { if (keyword && value) { const regex = new RegExp(`\\{\\{${keyword}\\}\\}`, 'g'); emailHtml = emailHtml.replace(regex, value); } }); } const subject = replaceTemplateVars(bigContent.subject, products.value[i]) || 'Thông báo từ Utopia Villas & Resort'; const response = await $insertapi( 'sendemail', { to: products.value[i].prdbk__transaction__customer__email, content: emailHtml, subject }, undefined, false, ); if (response !== null) { await $insertapi('productnote', { ref: products.value[i].id, user: $store.login.id, detail: `Đã gửi ${templateNameRef.value} cho sản phẩm ${products.value[i].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 populateProducts(idFilters = {}) { const productsData = await $getdata( 'product', undefined, { filter: { ...filter.value, ...idFilters, }, sort: 'prdbk__transaction__resvtxn__psh__cycle', values: 'prdbk__transaction,prdbk__transaction__code,prdbk__transaction__date,prdbk__transaction__txncurrent__detail__status__name,prdbk,prdbk__transaction__policy__code,prdbk__transaction__sale_price,prdbk__transaction__discount_amount,prdbk__transaction__customer,prdbk__transaction__customer__code,prdbk__transaction__customer__type__code,prdbk__transaction__customer__phone,prdbk__transaction__customer__fullname,prdbk__transaction__customer__legal_code,prdbk__transaction__customer__issued_date,prdbk__transaction__customer__contact_address,prdbk__transaction__customer__address,prdbk__transaction__customer__email,prdbk__transaction__customer__orgncust__bank_account,prdbk__transaction__customer__orgncust__bank_name,prdbk__transaction__customer__orgncust__orgrep__people__fullname,prdbk__transaction__customer__orgncust__orgrep__relation__name,prdbk__transaction__co_op,prdbk__transaction__co_op__people,prdbk__transaction__co_op__people__fullname,prdbk__transaction__co_op__people__legal_code,prdbk__transaction__co_op__people__address,prdbk__transaction__co_op__people__contact_address,prdbk__transaction__co_op__people__phone,prdbk__transaction__amount_remain,prdbk__transaction__amount_received,prdbk__transaction__resvtxn__psh,prdbk__transaction__resvtxn__psh__code,prdbk__transaction__resvtxn__psh__amount_remain,prdbk__transaction__resvtxn__psh__paid_amount,prdbk__transaction__resvtxn__psh__penalty_amount,prdbk__transaction__resvtxn__psh__penalty_remain,prdbk__transaction__resvtxn__psh__remain_amount,prdbk__transaction__resvtxn__psh__amount,prdbk__transaction__resvtxn__psh__cycle,prdbk__transaction__resvtxn__psh__to_date,locked_until,note,cart,cart__name,cart__code,cart__dealer,cart__dealer__code,cart__dealer__name,direction,type,zone_type,dealer,link,type__name,dealer__code,dealer__name,id,code,trade_code,land_lot_code,zone_code,zone_type__name,lot_area,building_area,total_built_area,number_of_floors,land_lot_size,origin_price,price_excluding_vat,direction__name,villa_model,product_type,template_name,project,project__name,status,status__code,status__name,status__color,status__sale_status,status__sale_status__color,create_time,elpro__stage,elpro__area_before,elpro__current_area,elpro__area_gap,elpro__rate_amount,elpro__date', } ); // group by trade_code first products.value = groupByProduct(productsData); contents.value = products.value.map((product) => { const qr = templateNameRef.value === 'Mail Thông báo bàn giao đất' ? buildQrHtml($paymentQR(buildContentPayment(product), 'TCB')) : ''; const content = emailTemplate.value?.content.content + qr; const message = quillToEmailHtml(replaceTemplateVars(content, product)); // replace property `content` with `message`, then fill info return { ...emailTemplate.value?.content, message, content: undefined, } }); } /** * Group product objects by same product id * [{G.B01}, {G.B01}, {G.B01}] => {G.B01, with sum amount, sum remain_amount, etc.} */ function groupByProduct(products) { const uniqueProducts = groupBy(products, (p) => p.trade_code); const uniqueProductsWithSummedFields = Object.entries(uniqueProducts).map(([_, sameProductRows]) => { // same product different schedules const summedFields = { prdbk__transaction__resvtxn__psh__amount: sameProductRows.reduce((prev, curr) => prev + curr.prdbk__transaction__resvtxn__psh__amount, 0), prdbk__transaction__resvtxn__psh__paid_amount: sameProductRows.reduce((prev, curr) => prev + curr.prdbk__transaction__resvtxn__psh__paid_amount, 0), prdbk__transaction__resvtxn__psh__amount_remain: sameProductRows.reduce((prev, curr) => prev + curr.prdbk__transaction__resvtxn__psh__amount_remain, 0), prdbk__transaction__resvtxn__psh__penalty_remain: sameProductRows.reduce((prev, curr) => prev + curr.prdbk__transaction__resvtxn__psh__penalty_remain, 0), prdbk__transaction__resvtxn__psh__remain_amount: sameProductRows.reduce((prev, curr) => prev + curr.prdbk__transaction__resvtxn__psh__remain_amount, 0), } // console.table(summedFields); const productWithSummedFields = { ...last(sameProductRows), ...summedFields } return productWithSummedFields; }); return uniqueProductsWithSummedFields; } async function populateEmailTemplate() { if (templateNameRef.value) { const emailTemplateData = await $getdata( 'emailtemplate', { name__icontains: templateNameRef.value }, undefined, true, ); emailTemplate.value = emailTemplateData; } } onMounted(async () => { await populateEmailTemplate(); populateProducts(); }); watch(filter, () => { populateProducts(); }, { deep: true }); watch(() => $store.selectedProductsForHandoverEmail, (val) => { populateProducts({ id__in: val }); }) return { contents, send, isSending }; }