Initial commit
This commit is contained in:
70
app/components/handover/Handover.vue
Normal file
70
app/components/handover/Handover.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import DataView from '~/components/datatable/DataView.vue'
|
||||
import Modal from '~/components/Modal.vue'
|
||||
|
||||
const { $store } = useNuxtApp();
|
||||
const showmodal = ref();
|
||||
const products = ref([]);
|
||||
|
||||
function openConfirmModal() {
|
||||
showmodal.value = {
|
||||
component: 'handover/SelectTemplate',
|
||||
title: 'Xác nhận',
|
||||
width: '700px',
|
||||
height: '250px',
|
||||
vbind: { products }
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAll() {
|
||||
if ($store.selectedProductsForHandoverEmail.length === 0) {
|
||||
$store.commit('selectedProductsForHandoverEmail', products.value.map(p => p.id))
|
||||
} else {
|
||||
$store.commit('selectedProductsForHandoverEmail', [])
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<div class="buttons is-justify-content-end" v-if="$store.selectedProductsForHandoverEmail !== undefined">
|
||||
<button
|
||||
v-if="$store.selectedProductsForHandoverEmail.length > 0"
|
||||
@click="openConfirmModal()"
|
||||
class="button is-light"
|
||||
>
|
||||
Gửi {{ $store.selectedProductsForHandoverEmail.length }} thông báo
|
||||
</button>
|
||||
<button
|
||||
@click="toggleAll"
|
||||
class="button"
|
||||
:disabled="products.length === 0"
|
||||
>
|
||||
{{ $store.selectedProductsForHandoverEmail.length > 0 ? 'Bỏ chọn' : 'Chọn' }} tất cả
|
||||
</button>
|
||||
</div>
|
||||
<DataView v-bind="{
|
||||
api: 'product',
|
||||
params: {
|
||||
filter: { elpro__stage__gt: 0 },
|
||||
values: 'prdbk__transaction__code,prdbk__transaction__txncurrent__detail__status__name,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__co_op,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__stage__name,elpro__area_before,elpro__current_area,elpro__area_gap,elpro__rate_amount,elpro__date,elpro__eligibility_date,elpro__status',
|
||||
distinct_values: {
|
||||
label: { type: 'Concat', field: ['trade_code', 'type__name', 'land_lot_size', 'zone_type__name', 'status__name'] },
|
||||
count_note: { type: 'Count', field: 'prdnote' }
|
||||
},
|
||||
summary: 'annotate',
|
||||
},
|
||||
setting: 'handover',
|
||||
pagename: 'pagedata-handover',
|
||||
modal: { component: 'parameter/ProductForm', title: 'Sản phẩm' },
|
||||
realtime: { time: 2, update: 'false' },
|
||||
onDisplayDataChange: (values) => products = values
|
||||
}"
|
||||
/>
|
||||
<Modal
|
||||
v-if="showmodal"
|
||||
v-bind="showmodal"
|
||||
@close="showmodal = undefined"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
26
app/components/handover/HandoverCheckbox.vue
Normal file
26
app/components/handover/HandoverCheckbox.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
productId: Number,
|
||||
});
|
||||
const { $store } = useNuxtApp();
|
||||
$store.commit('selectedProductsForHandoverEmail', []);
|
||||
</script>
|
||||
<template>
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="$store.selectedProductsForHandoverEmail.includes(props.productId)"
|
||||
@change="(e) => {
|
||||
if (e.target.checked) {
|
||||
$store.commit(
|
||||
'selectedProductsForHandoverEmail',
|
||||
[ ...$store.selectedProductsForHandoverEmail, props.productId],
|
||||
)
|
||||
} else {
|
||||
$store.commit(
|
||||
'selectedProductsForHandoverEmail',
|
||||
$store.selectedProductsForHandoverEmail.filter(x => x !== props.productId),
|
||||
)
|
||||
}
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
107
app/components/handover/HandoverSendEmail.vue
Normal file
107
app/components/handover/HandoverSendEmail.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<script setup>
|
||||
import Template1 from '@/lib/email/templates/Template1.vue';
|
||||
import useSendHandoverEmail from './useSendHandoverEmail';
|
||||
|
||||
const props = defineProps({
|
||||
row: Object,
|
||||
})
|
||||
|
||||
const { $getdata, $store } = useNuxtApp();
|
||||
$store.commit('selectedProductsForHandoverEmail', [props.row.id]);
|
||||
const handoverTemplates = ref([]);
|
||||
const selectedHandoverTemplate = ref(null);
|
||||
const filter = ref({ id: props.row.id, elpro__stage__gt: 0 });
|
||||
const templateNameRef = computed(() => selectedHandoverTemplate.value?.name);
|
||||
|
||||
const allowSend = computed(() => {
|
||||
if (!templateNameRef.value) return false;
|
||||
if (templateNameRef.value === 'Biên bản bàn giao đất - Cá nhân') {
|
||||
// disallow submit if product doesn't have CN customer
|
||||
const allCn = props.row.prdbk__transaction__customer__type__code === 'CN'
|
||||
&& !props.row.prdbk__transaction__co_op;
|
||||
return allCn;
|
||||
}
|
||||
if (templateNameRef.value === 'Biên bản bàn giao đất - Tổ chức') {
|
||||
// disallow submit if product doesn't have TC customer
|
||||
const allTc = props.row.prdbk__transaction__customer__type__code === 'TC';
|
||||
return allTc;
|
||||
}
|
||||
if (templateNameRef.value === 'Biên bản bàn giao đất - Đồng sở hữu') {
|
||||
// disallow submit if product doesn't have coop customer
|
||||
const allDsh = Boolean(props.row.prdbk__transaction__co_op);
|
||||
return allDsh;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
|
||||
const handoverEmailTemplateNames = [
|
||||
'Mail Thông báo bàn giao đất',
|
||||
'Mail Thông báo tất toán',
|
||||
'Biên bản bàn giao đất - Cá nhân',
|
||||
'Biên bản bàn giao đất - Đồng sở hữu',
|
||||
'Biên bản bàn giao đất - Tổ chức',
|
||||
]
|
||||
|
||||
onMounted(async () => {
|
||||
const templatesData = await $getdata('emailtemplate', undefined, {
|
||||
filter: { name__in: handoverEmailTemplateNames },
|
||||
sort: 'name'
|
||||
});
|
||||
handoverTemplates.value = templatesData;
|
||||
});
|
||||
|
||||
const showmodal = ref();
|
||||
|
||||
function openConfirmModal() {
|
||||
showmodal.value = {
|
||||
component: 'dialog/Confirm',
|
||||
title: 'Xác nhận',
|
||||
width: '500px',
|
||||
height: 'auto',
|
||||
vbind: {
|
||||
content: `Bạn có đồng ý gửi ${$store.selectedProductsForHandoverEmail.length} ${templateNameRef.value} không?`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { contents, send, isSending } = useSendHandoverEmail(filter, templateNameRef);
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<div class="field">
|
||||
<label class="label">
|
||||
<span class="icon-text">
|
||||
<span>Chọn mẫu email<b class="ml-1 has-text-danger">*</b></span>
|
||||
</span>
|
||||
</label>
|
||||
<div class="tags">
|
||||
<span
|
||||
v-for="temp in handoverTemplates"
|
||||
:key="temp.id"
|
||||
:class="['tag is-hoverable fs-14', {'is-primary': temp.id === selectedHandoverTemplate?.id }]"
|
||||
@click="selectedHandoverTemplate = temp"
|
||||
>
|
||||
{{ temp.name }}
|
||||
</span>
|
||||
</div>
|
||||
<p v-if="!allowSend && templateNameRef" class="help is-danger">Mẫu email không hợp lệ với sản phẩm</p>
|
||||
</div>
|
||||
<Template1 v-if="contents && allowSend" :content="contents[0]" previewMode />
|
||||
<div class="is-flex is-justify-content-center mt-4">
|
||||
<button
|
||||
:class="['button is-primary', { 'is-loading': isSending }]"
|
||||
:disabled="!allowSend"
|
||||
@click="openConfirmModal()"
|
||||
>
|
||||
Gửi thông báo
|
||||
</button>
|
||||
</div>
|
||||
<Modal
|
||||
v-if="showmodal"
|
||||
v-bind="showmodal"
|
||||
@close="showmodal = undefined"
|
||||
@confirm="send"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
9
app/components/handover/HandoverStatus.vue
Normal file
9
app/components/handover/HandoverStatus.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
status: Boolean
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<span v-if="status" class="has-text-primary">Đã bàn giao</span>
|
||||
<span v-else class="has-text-danger">Chưa bàn giao</span>
|
||||
</template>
|
||||
104
app/components/handover/SelectTemplate.vue
Normal file
104
app/components/handover/SelectTemplate.vue
Normal file
@@ -0,0 +1,104 @@
|
||||
<script setup>
|
||||
import useSendHandoverEmail from '@/components/handover/useSendHandoverEmail';
|
||||
|
||||
const props = defineProps({
|
||||
products: Array
|
||||
});
|
||||
|
||||
const { $getdata, $store } = useNuxtApp();
|
||||
const emit = defineEmits(['close', 'modalevent']);
|
||||
function cancel() {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
async function confirm() {
|
||||
await send();
|
||||
emit('modalevent', { name: 'confirm'} );
|
||||
emit('close');
|
||||
}
|
||||
|
||||
const handoverTemplates = ref([]);
|
||||
const selectedHandoverTemplate = ref(null);
|
||||
const filter = ref({ elpro__stage__gt: 0 });
|
||||
const templateNameRef = computed(() => selectedHandoverTemplate.value?.name);
|
||||
const { send, isSending } = useSendHandoverEmail(filter, templateNameRef);
|
||||
|
||||
const allowSend = computed(() => {
|
||||
if (!templateNameRef.value) return false;
|
||||
if (templateNameRef.value === 'Biên bản bàn giao đất - Cá nhân') {
|
||||
// disallow submit if not all products have CN customers
|
||||
const allCn = props.products.every(p =>
|
||||
p.prdbk__transaction__customer__type__code === 'CN'
|
||||
&& !p.prdbk__transaction__co_op
|
||||
);
|
||||
return allCn;
|
||||
}
|
||||
if (templateNameRef.value === 'Biên bản bàn giao đất - Tổ chức') {
|
||||
// disallow submit if not all products have TC customers
|
||||
const allTc = props.products.every(p => p.prdbk__transaction__customer__type__code === 'TC');
|
||||
return allTc;
|
||||
}
|
||||
if (templateNameRef.value === 'Biên bản bàn giao đất - Đồng sở hữu') {
|
||||
// disallow submit if not all products have coop customers
|
||||
const allDsh = props.products.every(p => Boolean(p.prdbk__transaction__co_op));
|
||||
return allDsh;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const handoverEmailTemplateNames = [
|
||||
'Mail Thông báo bàn giao đất',
|
||||
'Mail Thông báo tất toán',
|
||||
'Biên bản bàn giao đất - Cá nhân',
|
||||
'Biên bản bàn giao đất - Đồng sở hữu',
|
||||
'Biên bản bàn giao đất - Tổ chức',
|
||||
];
|
||||
|
||||
onMounted(async () => {
|
||||
const templatesData = await $getdata('emailtemplate', undefined, {
|
||||
filter: { name__in: handoverEmailTemplateNames },
|
||||
sort: 'name'
|
||||
});
|
||||
handoverTemplates.value = templatesData;
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<div class="field">
|
||||
<label class="label">
|
||||
<span class="icon-text">
|
||||
<span>Chọn mẫu email<b class="ml-1 has-text-danger">*</b></span>
|
||||
</span>
|
||||
</label>
|
||||
<div class="tags">
|
||||
<span
|
||||
v-for="temp in handoverTemplates"
|
||||
:key="temp.id"
|
||||
:class="['tag is-hoverable fs-14', {'is-primary': temp.id === selectedHandoverTemplate?.id }]"
|
||||
@click="selectedHandoverTemplate = temp"
|
||||
>
|
||||
{{ temp.name }}
|
||||
</span>
|
||||
</div>
|
||||
<p v-if="!allowSend && templateNameRef" class="help is-danger">Mẫu email không hợp lệ với các sản phẩm đã chọn</p>
|
||||
</div>
|
||||
<div>
|
||||
<p v-if="allowSend" class="my-4">
|
||||
Bạn có đồng ý gửi {{$store.selectedProductsForHandoverEmail.length}} thông báo không?
|
||||
</p>
|
||||
<div class="field is-grouped">
|
||||
<div class="control is-expanded">
|
||||
<button
|
||||
:class="['button is-primary has-text-white', { 'is-loading': isSending }]"
|
||||
@click="confirm()"
|
||||
:disabled="!allowSend"
|
||||
>
|
||||
Đồng ý
|
||||
</button>
|
||||
<button class="button is-white ml-2" @click="cancel()">Hủy</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
378
app/components/handover/useSendHandoverEmail.js
Normal file
378
app/components/handover/useSendHandoverEmail.js
Normal file
@@ -0,0 +1,378 @@
|
||||
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) => `
|
||||
<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(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(
|
||||
/<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}"`;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
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 } = 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(/<tr\s+class=["']header-row["'][^>]*>[\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 };
|
||||
}
|
||||
Reference in New Issue
Block a user