Files
web/app/components/debt/Due.vue
2026-05-07 16:15:33 +07:00

364 lines
12 KiB
Vue

<script setup>
import Template1 from "@/lib/email/templates/Template1.vue";
import { render } from "@vue-email/render";
import { forEachAsync, isEqual } from "es-toolkit";
const {
$dayjs,
$getdata,
$insertapi,
$numtoString,
$numberToVietnamese,
$numberToVietnameseCurrency,
$formatDateVN,
$getFirstAndLastName,
$snackbar,
$store,
} = useNuxtApp();
const payables = ref(null);
const defaultFilter = {
status: 1,
to_date__gte: $dayjs().format("YYYY-MM-DD"),
to_date__lte: undefined,
};
const filter = ref(defaultFilter);
const activeDateFilter = ref(null);
const key = ref(0);
function setDateFilter(detail) {
activeDateFilter.value = isEqual(activeDateFilter.value, detail) ? null : detail;
}
function resetDateFilter() {
activeDateFilter.value = null;
}
onMounted(async () => {
const payablesData = await $getdata("bizsetting", undefined, {
filter: { classify: "duepayables" },
sort: "index",
});
payables.value = payablesData;
});
watch(
activeDateFilter,
(val) => {
if (!val) {
filter.value = defaultFilter;
contents.value = null;
} else {
const cutoffDate = $dayjs().add(val.time, "day").format("YYYY-MM-DD");
const filterField = `to_date__${val.lookup}`;
filter.value = {
...defaultFilter,
[filterField]: cutoffDate,
};
}
},
{ deep: true },
);
const contents = ref(null);
const isSending = ref(false);
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 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}`}`;
}
};
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,
from_date,
to_date,
remain_amount,
cycle,
} = 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\.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}`}` || "");
}
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, "")
);
}
const showmodal = ref(null);
function openConfirmModal() {
showmodal.value = {
component: "dialog/Confirm",
title: "Xác nhận",
width: "500px",
height: "100px",
vbind: {
content: "Bạn có đồng ý gửi thông báo hàng loạt không?",
},
};
}
async function sendEmails() {
isSending.value = true;
$snackbar("Hệ thống đang xử lý ngầm yêu cầu gửi email hàng loạt...");
const paymentScheduleData = await $getdata("payment_schedule", undefined, {
filter: filter.value,
sort: "to_date",
values:
"penalty_paid,penalty_remain,penalty_amount,penalty_reduce,batch_date,amount_remain,paid_amount,remain_amount,code,status,txn_detail,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",
});
const emailTemplate = await $getdata("emailtemplate", { id: activeDateFilter.value.emailTemplate }, undefined, true);
let message = emailTemplate.content.content;
contents.value = paymentScheduleData.map((paymentSchedule) => {
message = message.replace(/<div[^>]*>\s*<img[^>]*img\.vietqr\.io[^>]*>\s*<\/div>/g, "");
const transfer = {
bank: {
code: "MB",
name: "MB Bank",
},
account: {
number: "146768686868",
name: "CONG TY CO PHAN BAT DONG SAN UTOPIA",
},
content: "Thanh toán đơn #xyz",
};
transfer.content = buildContentPayment(paymentSchedule);
const params = new URLSearchParams({
addInfo: transfer.content,
accountName: transfer.account.name,
});
const qrImageUrl = `https://img.vietqr.io/image/${transfer.bank.code}-${transfer.account.number}-print.png?${params.toString()}`;
message = `
${message.trim()}
${buildQrHtml(qrImageUrl)}
`;
return {
...emailTemplate.content,
content: undefined,
message: replaceTemplateVars(message, paymentSchedule),
};
});
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: paymentScheduleData[i].txn_detail__transaction__customer__email,
content: finalEmailHtml,
subject: replaceTemplateVars(subject, paymentScheduleData[i]) || "Thông báo từ Utopia Villas & Resort",
},
undefined,
false,
);
if (response !== null) {
await $insertapi(
"productnote",
{
ref: paymentScheduleData[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 ${paymentScheduleData[i].txn_detail__transaction__product__trade_code} vào lúc ${$dayjs().format("HH:mm ngày L")}.`,
},
undefined,
false,
);
}
});
setTimeout(() => {
$snackbar("Thông báo đã được gửi thành công đến các khách hàng.");
isSending.value = false;
}, 1000);
}
watch(
filter,
() => {
key.value += 1;
},
{ deep: true },
);
</script>
<template>
<div class="is-flex is-justify-content-space-between is-align-content-center mb-4">
<div class="buttons m-0">
<p>Đến hạn:</p>
<button
v-for="payable in payables"
:key="payable.id"
@click="setDateFilter(payable.detail)"
:class="['button', { 'is-primary': isEqual(activeDateFilter, payable.detail) }]"
>
{{ payable.detail.lookup === "lte" ? "≤" : ">" }}
{{ payable.detail.time }} ngày
</button>
<button
v-if="activeDateFilter"
@click="resetDateFilter()"
class="button is-white"
>
Xoá lọc
</button>
</div>
<button
v-if="activeDateFilter"
@click="openConfirmModal()"
:class="['button', 'is-light', { 'is-loading': isSending }]"
>
Gửi thông báo
</button>
</div>
<DataView
:key="key"
v-bind="{
pagename: 'payment-schedule-due',
api: 'payment_schedule',
setting: 'payment-schedule-debt-due',
realtime: { time: '5', update: 'true' },
params: {
filter,
sort: 'to_date',
values:
'penalty_paid,penalty_remain,penalty_amount,penalty_reduce,batch_date,ovd_days,amount_remain,paid_amount,remain_amount,code,status,txn_detail,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__policy__code,txn_detail__phase__name,type__name,from_date,to_date,amount,cycle,cycle_days,status__name,detail,entry',
},
}"
/>
<Modal
v-if="showmodal"
v-bind="showmodal"
@confirm="sendEmails()"
@close="showmodal = undefined"
/>
<!-- <div class="is-flex is-gap-1">
// debug
<Template1
v-if="contents"
v-for="content in contents"
:content="content"
previewMode
/>
</div> -->
</template>