This commit is contained in:
Viet An
2026-04-07 10:08:00 +07:00
parent f8bdfc98fc
commit 7402e61906
9 changed files with 673 additions and 436 deletions

View File

@@ -166,20 +166,20 @@ const menu = [
if ($store.rights.length > 0) { if ($store.rights.length > 0) {
menu = menu.filter((v) => $findIndex($store.rights, { setting: v.id }) >= 0); menu = menu.filter((v) => $findIndex($store.rights, { setting: v.id }) >= 0);
} }
if (menu.length === 0) { // if (menu.length === 0) {
$snackbar( // $snackbar(
$store.lang === 'vi' // $store.lang === 'vi'
? 'Bạn không có quyền truy cập' // ? 'Bạn không có quyền truy cập'
: 'You do not have permission to access.', // : 'You do not have permission to access.',
); // );
} // }
menu.map((v) => { // menu.map((v) => {
let arr = $filter($store.common, { category: 'submenu', classify: v.code }); // let arr = $filter($store.common, { category: 'submenu', classify: v.code });
if ($store.rights.length > 0) { // if ($store.rights.length > 0) {
arr = arr.filter((x) => $findIndex($store.rights, { setting: x.id }) >= 0); // arr = arr.filter((x) => $findIndex($store.rights, { setting: x.id }) >= 0);
} // }
v.submenu = arr.length > 0 ? arr : null; // v.submenu = arr.length > 0 ? arr : null;
}); // });
const leftmenu = $filter(menu, { category: 'topmenu', classify: 'left' }); const leftmenu = $filter(menu, { category: 'topmenu', classify: 'left' });
let currentTab = ref(leftmenu.length > 0 ? leftmenu[0] : undefined); let currentTab = ref(leftmenu.length > 0 ? leftmenu[0] : undefined);
const subTab = ref(); const subTab = ref();

View File

@@ -2,24 +2,22 @@
import useSendEmail from '@/components/debt/useSendEmail'; import useSendEmail from '@/components/debt/useSendEmail';
import { isEqual } from 'es-toolkit'; import { isEqual } from 'es-toolkit';
const { const { $dayjs, $getdata, $store } = useNuxtApp();
$dayjs,
$getdata,
$store,
} = useNuxtApp();
const payables = ref(null); const payables = ref(null);
const defaultFilter = { const defaultFilter = {
status: 1, status: 1,
to_date__gte: $dayjs().format('YYYY-MM-DD'), to_date__gte: $dayjs().format('YYYY-MM-DD'),
to_date__lte: undefined, to_date__lte: undefined,
} };
const filter = ref(defaultFilter); const filter = ref(defaultFilter);
const activeDateFilter = ref(null); const activeDateFilter = ref(null);
const key = ref(0); const key = ref(0);
function setDateFilter(detail) { function setDateFilter(detail) {
activeDateFilter.value = isEqual(activeDateFilter.value, detail) ? null : detail; activeDateFilter.value = isEqual(activeDateFilter.value, detail)
? null
: detail;
} }
function resetDateFilter() { function resetDateFilter() {
@@ -28,12 +26,14 @@ function resetDateFilter() {
const paymentSchedules = ref([]); const paymentSchedules = ref([]);
onMounted(async () => { // onMounted(async () => {
const payablesData = await $getdata('bizsetting', undefined, { filter: { classify: 'duepayables' }, sort: 'index' }); // const payablesData = await $getdata('bizsetting', undefined, { filter: { classify: 'duepayables' }, sort: 'index' });
payables.value = payablesData; // payables.value = payablesData;
}); // });
watch(activeDateFilter, (val) => { watch(
activeDateFilter,
(val) => {
if (!val) { if (!val) {
filter.value = defaultFilter; filter.value = defaultFilter;
} else { } else {
@@ -42,9 +42,11 @@ watch(activeDateFilter, (val) => {
filter.value = { filter.value = {
...defaultFilter, ...defaultFilter,
[filterField]: cutoffDate, [filterField]: cutoffDate,
};
} }
} },
}, { deep: true }) { deep: true },
);
const showmodal = ref(null); const showmodal = ref(null);
@@ -56,38 +58,51 @@ function openConfirmModal() {
height: '100px', height: '100px',
vbind: { vbind: {
content: `Bạn có đồng ý gửi ${$store.selectedPaymentSchedulesForEmailInDue.length} thông báo đến hạn không?`, content: `Bạn có đồng ý gửi ${$store.selectedPaymentSchedulesForEmailInDue.length} thông báo đến hạn không?`,
} },
} };
} }
watch(filter, () => { watch(
filter,
() => {
key.value += 1; key.value += 1;
}, { deep: true }) },
{ deep: true },
);
watch(key, () => { watch(key, () => {
// reset when DataView re-renders because of filter // reset when DataView re-renders because of filter
$store.commit('selectedPaymentSchedulesForEmailInDue', []) $store.commit('selectedPaymentSchedulesForEmailInDue', []);
}) });
function toggleAll() { function toggleAll() {
if ($store.selectedPaymentSchedulesForEmailInDue.length === 0) { if ($store.selectedPaymentSchedulesForEmailInDue.length === 0) {
$store.commit('selectedPaymentSchedulesForEmailInDue', paymentSchedules.value.map(p => p.id)) $store.commit(
'selectedPaymentSchedulesForEmailInDue',
paymentSchedules.value.map((p) => p.id),
);
} else { } else {
$store.commit('selectedPaymentSchedulesForEmailInDue', []) $store.commit('selectedPaymentSchedulesForEmailInDue', []);
} }
} }
const { contents, send, isSending } = useSendEmail(filter, 13); const { contents, send, isSending } = useSendEmail(filter, 13);
</script> </script>
<template> <template>
<div class="is-flex is-justify-content-space-between is-align-content-center mb-4"> <div
class="is-flex is-justify-content-space-between is-align-content-center mb-4"
>
<div class="buttons m-0"> <div class="buttons m-0">
<p>Đến hạn:</p> <p>Đến hạn:</p>
<button <button
v-for="payable in payables" v-for="payable in payables"
:key="payable.id" :key="payable.id"
@click="setDateFilter(payable.detail)" @click="setDateFilter(payable.detail)"
:class="['button', { 'is-primary': isEqual(activeDateFilter, payable.detail) }]" :class="[
'button',
{ 'is-primary': isEqual(activeDateFilter, payable.detail) },
]"
> >
{{ payable.detail.lookup === 'lte' ? '≤' : '>' }} {{ payable.detail.time }} ngày {{ payable.detail.lookup === 'lte' ? '≤' : '>' }}
{{ payable.detail.time }} ngày
</button> </button>
<button <button
v-if="activeDateFilter" v-if="activeDateFilter"
@@ -97,7 +112,10 @@ const { contents, send, isSending } = useSendEmail(filter, 13);
Xoá lọc Xoá lọc
</button> </button>
</div> </div>
<div class="buttons" v-if="$store.selectedPaymentSchedulesForEmailInDue !== undefined"> <div
class="buttons"
v-if="$store.selectedPaymentSchedulesForEmailInDue !== undefined"
>
<button <button
v-if="$store.selectedPaymentSchedulesForEmailInDue.length > 0" v-if="$store.selectedPaymentSchedulesForEmailInDue.length > 0"
@click="openConfirmModal()" @click="openConfirmModal()"
@@ -110,7 +128,12 @@ const { contents, send, isSending } = useSendEmail(filter, 13);
class="button" class="button"
:disabled="paymentSchedules.length === 0" :disabled="paymentSchedules.length === 0"
> >
{{ $store.selectedPaymentSchedulesForEmailInDue.length > 0 ? 'Bỏ chọn' : 'Chọn' }} tất cả {{
$store.selectedPaymentSchedulesForEmailInDue.length > 0
? 'Bỏ chọn'
: 'Chọn'
}}
tất cả
</button> </button>
</div> </div>
</div> </div>
@@ -124,9 +147,10 @@ const { contents, send, isSending } = useSendEmail(filter, 13);
params: { params: {
filter, filter,
sort: 'to_date', 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__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', 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__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',
}, },
onDisplayDataChange: (values) => paymentSchedules = values onDisplayDataChange: (values) => (paymentSchedules = values),
}" }"
/> />
<Modal <Modal

View File

@@ -2,23 +2,21 @@
import useSendEmail from '@/components/debt/useSendEmail'; import useSendEmail from '@/components/debt/useSendEmail';
import { isEqual } from 'es-toolkit'; import { isEqual } from 'es-toolkit';
const { const { $dayjs, $getdata, $store } = useNuxtApp();
$dayjs,
$getdata,
$store,
} = useNuxtApp();
const payables = ref(null); const payables = ref(null);
const defaultFilter = { const defaultFilter = {
status: 1, status: 1,
to_date__lt: $dayjs().format('YYYY-MM-DD'), to_date__lt: $dayjs().format('YYYY-MM-DD'),
} };
const filter = ref(defaultFilter); const filter = ref(defaultFilter);
const activeDateFilter = ref(null); const activeDateFilter = ref(null);
const key = ref(0); const key = ref(0);
function setDateFilter(detail) { function setDateFilter(detail) {
activeDateFilter.value = isEqual(activeDateFilter.value, detail) ? null : detail; activeDateFilter.value = isEqual(activeDateFilter.value, detail)
? null
: detail;
} }
function resetDateFilter() { function resetDateFilter() {
@@ -26,40 +24,42 @@ function resetDateFilter() {
} }
const paymentSchedule = ref([]); const paymentSchedule = ref([]);
const paymentScheduleValues = 'id,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 paymentScheduleValues =
'id,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';
async function setPaymentSheduleData() { async function setPaymentSheduleData() {
const paymentScheduleData = await $getdata( const paymentScheduleData = await $getdata('payment_schedule', undefined, {
'payment_schedule',
undefined,
{
filter: filter.value, filter: filter.value,
sort: 'to_date', sort: 'to_date',
values: paymentScheduleValues, values: paymentScheduleValues,
} });
);
paymentSchedule.value = paymentScheduleData; paymentSchedule.value = paymentScheduleData;
} }
onMounted(async () => { // onMounted(async () => {
setPaymentSheduleData(); // setPaymentSheduleData();
const payablesData = await $getdata('bizsetting', undefined, { filter: { classify: 'overduepayables' }, sort: 'index' }); // const payablesData = await $getdata('bizsetting', undefined, { filter: { classify: 'overduepayables' }, sort: 'index' });
payables.value = payablesData; // payables.value = payablesData;
}); // });
watch(activeDateFilter, (val) => { watch(
activeDateFilter,
(val) => {
if (!val) { if (!val) {
filter.value = defaultFilter; filter.value = defaultFilter;
} else { } else {
const cutoffDate = $dayjs().subtract(val.time, 'day').format('YYYY-MM-DD'); const cutoffDate = $dayjs()
const filterField = `to_date__${val.lookup === 'lte' ? 'gt' : .subtract(val.time, 'day')
'lte'}`; .format('YYYY-MM-DD');
const filterField = `to_date__${val.lookup === 'lte' ? 'gt' : 'lte'}`;
filter.value = { filter.value = {
...defaultFilter, ...defaultFilter,
[filterField]: cutoffDate, [filterField]: cutoffDate,
};
} }
} },
}, { deep: true }) { deep: true },
);
const showmodal = ref(null); const showmodal = ref(null);
@@ -71,40 +71,53 @@ function openConfirmModal() {
height: '100px', height: '100px',
vbind: { vbind: {
content: `Bạn có đồng ý gửi ${$store.selectedPaymentSchedulesForEmailInOverdue.length} thông báo quá hạn không?`, content: `Bạn có đồng ý gửi ${$store.selectedPaymentSchedulesForEmailInOverdue.length} thông báo quá hạn không?`,
} },
} };
} }
watch(filter, () => { watch(
filter,
() => {
key.value += 1; key.value += 1;
setPaymentSheduleData(); setPaymentSheduleData();
}, { deep: true }) },
{ deep: true },
);
watch(key, () => { watch(key, () => {
// reset when DataView re-renders because of filter // reset when DataView re-renders because of filter
$store.commit('selectedPaymentSchedulesForEmailInOverdue', []) $store.commit('selectedPaymentSchedulesForEmailInOverdue', []);
}) });
function toggleAll() { function toggleAll() {
if ($store.selectedPaymentSchedulesForEmailInOverdue.length === 0) { if ($store.selectedPaymentSchedulesForEmailInOverdue.length === 0) {
$store.commit('selectedPaymentSchedulesForEmailInOverdue', paymentSchedule.value.map(p => p.id)) $store.commit(
'selectedPaymentSchedulesForEmailInOverdue',
paymentSchedule.value.map((p) => p.id),
);
} else { } else {
$store.commit('selectedPaymentSchedulesForEmailInOverdue', []) $store.commit('selectedPaymentSchedulesForEmailInOverdue', []);
} }
} }
const { contents, send, isSending } = useSendEmail(filter, 14); const { contents, send, isSending } = useSendEmail(filter, 14);
</script> </script>
<template> <template>
<div class="is-flex is-justify-content-space-between is-align-content-center mb-4"> <div
class="is-flex is-justify-content-space-between is-align-content-center mb-4"
>
<div class="buttons m-0"> <div class="buttons m-0">
<p>Quá hạn:</p> <p>Quá hạn:</p>
<button <button
v-for="payable in payables" v-for="payable in payables"
:key="payable.id" :key="payable.id"
@click="setDateFilter(payable.detail)" @click="setDateFilter(payable.detail)"
:class="['button', { 'is-primary': isEqual(activeDateFilter, payable.detail) }]" :class="[
'button',
{ 'is-primary': isEqual(activeDateFilter, payable.detail) },
]"
> >
{{ payable.detail.lookup === 'lte' ? '≤' : '>' }} {{ payable.detail.time }} ngày {{ payable.detail.lookup === 'lte' ? '≤' : '>' }}
{{ payable.detail.time }} ngày
</button> </button>
<button <button
v-if="activeDateFilter" v-if="activeDateFilter"
@@ -114,19 +127,25 @@ const { contents, send, isSending } = useSendEmail(filter, 14);
Xoá lọc Xoá lọc
</button> </button>
</div> </div>
<div class="buttons" v-if="$store.selectedPaymentSchedulesForEmailInOverdue !== undefined"> <div
class="buttons"
v-if="$store.selectedPaymentSchedulesForEmailInOverdue !== undefined"
>
<button <button
v-if="$store.selectedPaymentSchedulesForEmailInOverdue.length > 0" v-if="$store.selectedPaymentSchedulesForEmailInOverdue.length > 0"
@click="openConfirmModal()" @click="openConfirmModal()"
:class="['button', 'is-light', { 'is-loading': isSending }]" :class="['button', 'is-light', { 'is-loading': isSending }]"
> >
Gửi {{ $store.selectedPaymentSchedulesForEmailInOverdue.length }} thông báo Gửi {{ $store.selectedPaymentSchedulesForEmailInOverdue.length }} thông
báo
</button> </button>
<button <button @click="toggleAll" class="button">
@click="toggleAll" {{
class="button" $store.selectedPaymentSchedulesForEmailInOverdue.length > 0
> ? 'Bỏ chọn'
{{ $store.selectedPaymentSchedulesForEmailInOverdue.length > 0 ? 'Bỏ chọn' : 'Chọn' }} tất cả : 'Chọn'
}}
tất cả
</button> </button>
</div> </div>
</div> </div>
@@ -141,7 +160,7 @@ const { contents, send, isSending } = useSendEmail(filter, 14);
filter, filter,
sort: 'to_date', sort: 'to_date',
values: paymentScheduleValues, values: paymentScheduleValues,
} },
}" }"
/> />
<Modal <Modal

View File

@@ -4,7 +4,11 @@
<div style="width: 200px"> <div style="width: 200px">
<div class="py-1" v-for="v in array"> <div class="py-1" v-for="v in array">
<a <a
:class="(current ? current.code === v.code : false) ? 'has-text-primary has-text-weight-bold' : ''" :class="
(current ? current.code === v.code : false)
? 'has-text-primary has-text-weight-bold'
: ''
"
@click="changeTab(v)" @click="changeTab(v)"
>{{ v.name }}</a >{{ v.name }}</a
> >
@@ -13,162 +17,105 @@
</div> </div>
<div class="column"> <div class="column">
<div class="fsb-20 mb-3" v-if="current">{{ current.name }}</div> <div class="fsb-20 mb-3" v-if="current">{{ current.name }}</div>
<DataView v-bind="current.vbind" v-if="current && current.typeView === 'table'"></DataView> <DataView
<component v-if="current && current.typeView === 'component'" :is="current.component" v-bind="current.vbind" /> v-bind="current.vbind"
v-if="current && current.typeView === 'table'"
></DataView>
<component
v-if="current && current.typeView === 'component'"
:is="current.component"
v-bind="current.vbind"
/>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
var array = [ var array = [
{ {
code: "transactionphase", code: 'transactionphase',
name: "Giai đoạn giao dịch", name: 'Giai đoạn giao dịch',
typeView: "table", typeView: 'table',
vbind: { vbind: {
api: "transactionphase", api: 'transactionphase',
setting: "transaction-phase", setting: 'transaction-phase',
pagename: "pagedata99", pagename: 'pagedata99',
timeopt: 36000, timeopt: 36000,
modal: { modal: {
component: "parameter/TransactionPhase", component: 'parameter/TransactionPhase',
title: "Transaction phase", title: 'Transaction phase',
height: "400px", height: '400px',
vbind: { api: "transactionphase" }, vbind: { api: 'transactionphase' },
}, },
}, },
}, },
{ {
code: "cart", code: 'cart',
name: "Danh sách giỏ hàng", name: 'Danh sách giỏ hàng',
typeView: "table", typeView: 'table',
vbind: { vbind: {
api: "cart", api: 'cart',
setting: "parameter-fields-cart", setting: 'parameter-fields-cart',
pagename: "parameter-fields-cart", pagename: 'parameter-fields-cart',
timeopt: 36000, timeopt: 36000,
modal: { modal: {
component: "parameter/CodeName", component: 'parameter/CodeName',
title: "Giỏ hàng", title: 'Giỏ hàng',
height: "400px", height: '400px',
vbind: { api: "cart" }, vbind: { api: 'cart' },
}, },
}, },
}, },
{ {
code: "documenttype", code: 'documenttype',
name: "Loại tài liệu", name: 'Loại tài liệu',
typeView: "table", typeView: 'table',
vbind: { vbind: {
api: "documenttype", api: 'documenttype',
setting: "parameter-fields", setting: 'parameter-fields',
pagename: "pagedata99", pagename: 'pagedata99',
timeopt: 36000, timeopt: 36000,
modal: { modal: {
component: "parameter/CodeName", component: 'parameter/CodeName',
title: "Document type", title: 'Document type',
height: "400px", height: '400px',
vbind: { api: "documenttype" }, vbind: { api: 'documenttype' },
}, },
}, },
}, },
{ {
code: "discounttype", code: 'discounttype',
name: "Danh sách chiết khấu", name: 'Danh sách chiết khấu',
typeView: "table", typeView: 'table',
vbind: { vbind: {
api: "discounttype", api: 'discounttype',
setting: "parameter-discount", setting: 'parameter-discount',
pagename: "tableDiscountType", pagename: 'tableDiscountType',
timeopt: 36000, timeopt: 36000,
modal: { modal: {
component: "parameter/DiscountType", component: 'parameter/DiscountType',
title: "Thông tin chiết khấu", title: 'Thông tin chiết khấu',
height: "400px", height: '400px',
vbind: { api: "discounttype" }, vbind: { api: 'discounttype' },
}, },
}, },
}, },
{ {
code: "gifttype", code: 'gifttype',
name: "Danh sách quà tặng", name: 'Danh sách quà tặng',
typeView: "table", typeView: 'table',
vbind: { vbind: {
api: "gift", api: 'gift',
setting: "parameter-gift", setting: 'parameter-gift',
pagename: "tableDiscountType", pagename: 'tableDiscountType',
timeopt: 36000, timeopt: 36000,
modal: { modal: {
component: "parameter/GiftType", component: 'parameter/GiftType',
title: "Thông tin quà tặng", title: 'Thông tin quà tặng',
height: "400px", height: '400px',
vbind: { api: "gift" }, vbind: { api: 'gift' },
}, },
}, },
}, },
{
code: "DuePayables",
name: "Lịch công nợ đến hạn",
typeView: "table",
vbind: {
api: "bizsetting",
params: {
filter: {
category: "system",
classify: "duepayables",
},
},
setting: "parameter-payable-schedule",
pagename: "tablePayableSchedule",
timeopt: 36000,
modal: {
component: "parameter/DuePayables",
title: "Thông tin lịch công nợ đến hạn",
height: "400px",
vbind: { api: "bizsetting" },
},
},
},
{
code: "OverduePayables",
name: "Lịch công nợ quá hạn",
typeView: "table",
vbind: {
api: "bizsetting",
params: {
filter: {
category: "system",
classify: "overduepayables",
},
},
setting: "parameter-overdue-payables",
pagename: "tableOverduePayables",
timeopt: 36000,
modal: {
component: "parameter/OverduePayables",
title: "Thông tin lịch công nợ quá hạn",
height: "400px",
vbind: { api: "bizsetting" },
},
},
},
// {
// code: "allocationRules",
// name: "Quy tắc phân bổ",
// typeView: "component",
// component: defineAsyncComponent(() => import("@/components/parameter/AllocationRules.vue")),
// vbind: {
// api: "common",
// setting: "parameter-gift",
// pagename: "tableDiscountType",
// timeopt: 36000,
// modal: {
// component: "parameter/AllocationRules",
// title: "Thông tin quà tặng",
// height: "400px",
// vbind: { api: "gift" },
// },
// },
// },
]; ];
var current = ref(array[0]); var current = ref(array[0]);
function changeTab(v) { function changeTab(v) {

View File

@@ -3,7 +3,11 @@
<div v-if="loadingData" class="has-text-centered py-5"> <div v-if="loadingData" class="has-text-centered py-5">
<SvgIcon v-bind="{ name: 'loading.svg', type: 'primary', size: 18 }" /> <SvgIcon v-bind="{ name: 'loading.svg', type: 'primary', size: 18 }" />
<p class="mt-2"> <p class="mt-2">
{{ isVietnamese ? "Đang tải thông tin công nợ..." : "Loading payment schedule information..." }} {{
isVietnamese
? 'Đang tải thông tin công nợ...'
: 'Loading payment schedule information...'
}}
</p> </p>
</div> </div>
@@ -12,95 +16,171 @@
<!-- Thông tin bản --> <!-- Thông tin bản -->
<div class="columns is-multiline is-mobile"> <div class="columns is-multiline is-mobile">
<div class="column is-3"> <div class="column is-3">
<strong>{{ isVietnamese ? "Mã:" : "Schedule Code:" }}</strong> <strong>{{ isVietnamese ? 'Mã:' : 'Schedule Code:' }}</strong>
<p>{{ paymentScheduleData.code || "-" }}</p> <p>{{ paymentScheduleData.code || '-' }}</p>
</div> </div>
<div class="column is-3"> <div class="column is-3">
<strong>{{ isVietnamese ? "Trạng thái:" : "Status:" }}</strong> <strong>{{ isVietnamese ? 'Trạng thái:' : 'Status:' }}</strong>
<p :class="{ <p
'has-text-success': paymentScheduleData.status__name === 'Đã xác nhận' || paymentScheduleData.status__name === 'Paid', :class="{
'has-text-warning': paymentScheduleData.status__name === 'Chưa xác nhận' || paymentScheduleData.status__name === 'Pending', 'has-text-success':
'has-text-danger': paymentScheduleData.status__name === 'Quá hạn' || paymentScheduleData.status__name === 'Overdue' paymentScheduleData.status__name === 'Đã xác nhận' ||
}"> paymentScheduleData.status__name === 'Paid',
{{ paymentScheduleData.status__name || "-" }} 'has-text-warning':
paymentScheduleData.status__name === 'Chưa xác nhận' ||
paymentScheduleData.status__name === 'Pending',
'has-text-danger':
paymentScheduleData.status__name === 'Quá hạn' ||
paymentScheduleData.status__name === 'Overdue',
}"
>
{{ paymentScheduleData.status__name || '-' }}
</p> </p>
</div> </div>
<div class="column is-3"> <div class="column is-3">
<strong>{{ isVietnamese ? "Loại thanh toán:" : "Payment Type:" }}</strong> <strong>{{
<p>{{ paymentScheduleData.type__name || "-" }}</p> isVietnamese ? 'Loại thanh toán:' : 'Payment Type:'
}}</strong>
<p>{{ paymentScheduleData.type__name || '-' }}</p>
</div> </div>
<div class="column is-3"> <div class="column is-3">
<strong>{{ isVietnamese ? "Đợt thanh toán:" : "Cycle:" }}</strong> <strong>{{ isVietnamese ? 'Đợt thanh toán:' : 'Cycle:' }}</strong>
<p>{{ paymentScheduleData.cycle_days || 0 }} ngày</p> <p>{{ paymentScheduleData.cycle_days || 0 }} ngày</p>
</div> </div>
<div class="column is-3"> <div class="column is-3">
<strong>{{ isVietnamese ? "Tiền gốc theo kỳ thanh toán:" : "Amount:" }}</strong> <strong>{{
<p class="has-text-weight-bold has-text-primary">{{ $numtoString(paymentScheduleData.amount) }}</p> isVietnamese ? 'Tiền gốc theo kỳ thanh toán:' : 'Amount:'
}}</strong>
<p class="has-text-weight-bold has-text-primary">
{{ $numtoString(paymentScheduleData.amount) }}
</p>
</div> </div>
<div class="column is-3"> <div class="column is-3">
<strong>{{ isVietnamese ? "Số tiền gốc đã thanh toán:" : "Paid Amount:" }}</strong> <strong>{{
<p class="has-text-weight-bold has-text-primary">{{ $numtoString(paymentScheduleData.paid_amount) }}</p> isVietnamese ? 'Số tiền gốc đã thanh toán:' : 'Paid Amount:'
}}</strong>
<p class="has-text-weight-bold has-text-primary">
{{ $numtoString(paymentScheduleData.paid_amount) }}
</p>
</div> </div>
<div class="column is-3"> <div class="column is-3">
<strong>{{ isVietnamese ? "Số tiền gốc còn lại:" : "Remaining Principal:" }}</strong> <strong>{{
<p class="has-text-weight-bold" isVietnamese ? 'Số tiền gốc còn lại:' : 'Remaining Principal:'
:class="paymentScheduleData.amount_remain > 0 ? 'has-text-danger' : 'has-text-success'"> }}</strong>
<p
class="has-text-weight-bold"
:class="
paymentScheduleData.amount_remain > 0
? 'has-text-danger'
: 'has-text-success'
"
>
{{ $numtoString(paymentScheduleData.amount_remain) }} {{ $numtoString(paymentScheduleData.amount_remain) }}
</p> </p>
</div> </div>
<div class="column is-3"> <div class="column is-3">
<strong>{{ isVietnamese ? "Ngày đến hạn:" : "Due Date:" }}</strong> <strong>{{ isVietnamese ? 'Ngày đến hạn:' : 'Due Date:' }}</strong>
<p :class="{ 'has-text-danger': isOverdue(paymentScheduleData.to_date && paymentScheduleData.ovd_days > 0) }"> <p
:class="{
'has-text-danger': isOverdue(
paymentScheduleData.to_date &&
paymentScheduleData.ovd_days > 0,
),
}"
>
{{ formatDate(paymentScheduleData.to_date) }} {{ formatDate(paymentScheduleData.to_date) }}
<span v-if="isOverdue(paymentScheduleData.to_date && paymentScheduleData.ovd_days > 0)" class="has-text-weight-bold"> <span
v-if="
isOverdue(
paymentScheduleData.to_date &&
paymentScheduleData.ovd_days > 0,
)
"
class="has-text-weight-bold"
>
(Quá hạn {{ paymentScheduleData.ovd_days }} ngày) (Quá hạn {{ paymentScheduleData.ovd_days }} ngày)
</span> </span>
</p> </p>
</div> </div>
<div class="column is-3"> <div class="column is-3">
<strong>{{ isVietnamese ? "Tổng lãi phải thu:" : "Total Penalty Amount:" }}</strong> <strong>{{
isVietnamese ? 'Tổng lãi phải thu:' : 'Total Penalty Amount:'
}}</strong>
<p class="has-text-weight-bold has-text-danger"> <p class="has-text-weight-bold has-text-danger">
{{ paymentScheduleData.penalty_amount > 0 ? $numtoString(paymentScheduleData.penalty_amount) : "-" }} {{
paymentScheduleData.penalty_amount > 0
? $numtoString(paymentScheduleData.penalty_amount)
: '-'
}}
</p> </p>
</div> </div>
<div class="column is-3"> <div class="column is-3">
<strong>{{ isVietnamese ? "Lãi phạt đã thanh toán:" : "Penalty Paid:" }}</strong> <strong>{{
isVietnamese ? 'Lãi phạt đã thanh toán:' : 'Penalty Paid:'
}}</strong>
<p class="has-text-weight-bold has-text-success"> <p class="has-text-weight-bold has-text-success">
{{ paymentScheduleData.penalty_paid > 0 ? $numtoString(paymentScheduleData.penalty_paid) : "-" }} {{
paymentScheduleData.penalty_paid > 0
? $numtoString(paymentScheduleData.penalty_paid)
: '-'
}}
</p> </p>
</div> </div>
<div class="column is-3"> <div class="column is-3">
<strong>{{ isVietnamese ? "Lãi phạt còn lại:" : "Penalty Remaining:" }}</strong> <strong>{{
isVietnamese ? 'Lãi phạt còn lại:' : 'Penalty Remaining:'
}}</strong>
<p class="has-text-weight-bold has-text-danger"> <p class="has-text-weight-bold has-text-danger">
{{ paymentScheduleData.penalty_remain > 0 ? $numtoString(paymentScheduleData.penalty_remain) : "-" }} {{
paymentScheduleData.penalty_remain > 0
? $numtoString(paymentScheduleData.penalty_remain)
: '-'
}}
</p> </p>
</div> </div>
<div class="column is-3"> <div class="column is-3">
<strong>{{ isVietnamese ? "Miễn giảm lãi phạt:" : "Penalty Reduced:" }}</strong> <strong>{{
isVietnamese ? 'Miễn giảm lãi phạt:' : 'Penalty Reduced:'
}}</strong>
<p class="has-text-weight-bold has-text-primary"> <p class="has-text-weight-bold has-text-primary">
{{ paymentScheduleData.penalty_reduce > 0 ? $numtoString(paymentScheduleData.penalty_reduce) : "-" }} {{
paymentScheduleData.penalty_reduce > 0
? $numtoString(paymentScheduleData.penalty_reduce)
: '-'
}}
</p> </p>
</div> </div>
<div class="column is-3"> <div class="column is-3">
<strong>{{ isVietnamese ? "Tổng tiền phải thanh toán:" : "Total Remaining:" }}</strong> <strong>{{
<p class="has-text-weight-bold has-text-primary">{{ $numtoString(paymentScheduleData.remain_amount) }}</p> isVietnamese ? 'Tổng tiền phải thanh toán:' : 'Total Remaining:'
}}</strong>
<p class="has-text-weight-bold has-text-primary">
{{ $numtoString(paymentScheduleData.remain_amount) }}
</p>
</div> </div>
<div class="column is-3"> <div class="column is-3">
<strong>{{ isVietnamese ? "Ghi chú:" : "Note:" }}</strong> <strong>{{ isVietnamese ? 'Ghi chú:' : 'Note:' }}</strong>
<p class="is-size-6">{{ paymentScheduleData.detail?.note || "-" }}</p> <p class="is-size-6">
{{ paymentScheduleData.detail?.note || '-' }}
</p>
</div> </div>
</div> </div>
<hr /> <hr />
<!-- Timeline lịch sử --> <!-- Timeline lịch sử -->
<div v-if="processedEntries.length > 0" class="is-flex is-flex-direction-column is-gap-5"> <div
v-if="processedEntries.length > 0"
<div v-for="(item, index) in processedEntries" :key="index" class="is-flex is-align-items-start is-gap-4"> class="is-flex is-flex-direction-column is-gap-5"
>
<div
v-for="(item, index) in processedEntries"
:key="index"
class="is-flex is-align-items-start is-gap-4"
>
<!-- ===================== REDUCTION ===================== --> <!-- ===================== REDUCTION ===================== -->
<template v-if="item.isReduction"> <template v-if="item.isReduction">
<div style="min-width: 3rem;"> <div style="min-width: 3rem">
<p class="is-size-5 has-text-weight-bold has-text-info"> <p class="is-size-5 has-text-weight-bold has-text-info">
{{ formatDate(item.entry.date) }} {{ formatDate(item.entry.date) }}
</p> </p>
@@ -112,23 +192,40 @@
<div class="is-flex-grow-1"> <div class="is-flex-grow-1">
<p class="is-size-5 has-text-weight-bold has-text-info mb-2"> <p class="is-size-5 has-text-weight-bold has-text-info mb-2">
Miễn giảm lãi phạt Miễn giảm lãi phạt
<span v-if="item.entry.code" class="tag is-info is-light ml-2"> <span
v-if="item.entry.code"
class="tag is-info is-light ml-2"
>
{{ item.entry.code }} {{ item.entry.code }}
</span> </span>
</p> </p>
<div class="box is-shadowless p-4" style="border-left: 5px solid #3273dc;"> <div
class="box is-shadowless p-4"
style="border-left: 5px solid #3273dc"
>
<div class="columns is-mobile is-multiline"> <div class="columns is-mobile is-multiline">
<div class="column is-6-mobile"> <div class="column is-6-mobile">
<span class="has-text-grey-light">Số tiền miễn giảm:</span><br /> <span class="has-text-grey-light">Số tiền miễn giảm:</span
<span class="has-text-info has-text-weight-semibold is-size-5"> ><br />
<span
class="has-text-info has-text-weight-semibold is-size-5"
>
{{ $numtoString(item.reduceAmount) }} {{ $numtoString(item.reduceAmount) }}
</span> </span>
</div> </div>
<div class="column is-6-mobile"> <div class="column is-6-mobile">
<span class="has-text-grey-light">Lãi còn lại sau miễn giảm:</span><br /> <span class="has-text-grey-light"
<span class="has-text-weight-semibold is-size-5" >Lãi còn lại sau miễn giảm:</span
:class="item.penaltyRemain > 0 ? 'has-text-danger' : 'has-text-success'"> ><br />
<span
class="has-text-weight-semibold is-size-5"
:class="
item.penaltyRemain > 0
? 'has-text-danger'
: 'has-text-success'
"
>
{{ $numtoString(item.penaltyRemain) }} {{ $numtoString(item.penaltyRemain) }}
</span> </span>
</div> </div>
@@ -139,7 +236,7 @@
<!-- ===================== PAYMENT ===================== --> <!-- ===================== PAYMENT ===================== -->
<template v-else> <template v-else>
<div style="min-width: 3rem;"> <div style="min-width: 3rem">
<p class="is-size-5 has-text-weight-bold has-text-primary"> <p class="is-size-5 has-text-weight-bold has-text-primary">
{{ formatDate(item.entry.date) }} {{ formatDate(item.entry.date) }}
</p> </p>
@@ -151,16 +248,25 @@
<div class="is-flex-grow-1"> <div class="is-flex-grow-1">
<p class="is-size-5 has-text-weight-bold has-text-dark mb-2"> <p class="is-size-5 has-text-weight-bold has-text-dark mb-2">
{{ getEntryTypeLabel(item.entry.type) }} {{ getEntryTypeLabel(item.entry.type) }}
<span v-if="item.entry.code" class="tag is-link is-light ml-2"> <span
v-if="item.entry.code"
class="tag is-link is-light ml-2"
>
{{ item.entry.code }} {{ item.entry.code }}
</span> </span>
</p> </p>
<div class="box is-shadowless p-4" style="border-left: 5px solid #204853;"> <div
class="box is-shadowless p-4"
style="border-left: 5px solid #204853"
>
<div class="columns is-mobile is-multiline is-gap-3"> <div class="columns is-mobile is-multiline is-gap-3">
<div class="column is-6-mobile"> <div class="column is-6-mobile">
<span class="has-text-grey-light">Gốc trả:</span><br /> <span class="has-text-grey-light">Gốc trả:</span><br />
<span v-if="item.entry.principal > 0" class="has-text-success has-text-weight-semibold is-size-5"> <span
v-if="item.entry.principal > 0"
class="has-text-success has-text-weight-semibold is-size-5"
>
{{ $numtoString(item.entry.principal) }} {{ $numtoString(item.entry.principal) }}
</span> </span>
<span v-else class="has-text-grey-light">-</span> <span v-else class="has-text-grey-light">-</span>
@@ -168,16 +274,26 @@
<div class="column is-6-mobile"> <div class="column is-6-mobile">
<span class="has-text-grey-light">Lãi trả:</span><br /> <span class="has-text-grey-light">Lãi trả:</span><br />
<span v-if="item.entry.penalty > 0" class="has-text-danger has-text-weight-semibold is-size-5"> <span
v-if="item.entry.penalty > 0"
class="has-text-danger has-text-weight-semibold is-size-5"
>
{{ $numtoString(item.entry.penalty) }} {{ $numtoString(item.entry.penalty) }}
</span> </span>
<span v-else class="has-text-grey-light">-</span> <span v-else class="has-text-grey-light">-</span>
</div> </div>
<div class="column is-6-mobile"> <div class="column is-6-mobile">
<span class="has-text-grey-light">Gốc còn lại:</span><br /> <span class="has-text-grey-light">Gốc còn lại:</span
<strong :class="item.principalRemain > 0 ? 'has-text-danger' : 'has-text-success'" ><br />
class="is-size-5"> <strong
:class="
item.principalRemain > 0
? 'has-text-danger'
: 'has-text-success'
"
class="is-size-5"
>
{{ $numtoString(item.principalRemain) }} {{ $numtoString(item.principalRemain) }}
</strong> </strong>
</div> </div>
@@ -185,15 +301,30 @@
<!-- Chi tiết lãi phát sinh --> <!-- Chi tiết lãi phát sinh -->
<div class=""> <div class="">
<p v-if="item.penaltyThisPeriod > 0" class="is-size-6 has-text-grey"> <p
nợ gốc còn lại: {{ $numtoString(item.principalBefore) }} × v-if="item.penaltyThisPeriod > 0"
{{ item.penaltyDetail.days }} ngày (từ {{ item.penaltyDetail.from }} đến {{ item.penaltyDetail.to }}) class="is-size-6 has-text-grey"
× {{ item.rate }}%/ngày = {{ $numtoString(item.penaltyThisPeriod) }} >
Dư nợ gốc còn lại:
{{ $numtoString(item.principalBefore) }} ×
{{ item.penaltyDetail.days }} ngày (từ
{{ item.penaltyDetail.from }} đến
{{ item.penaltyDetail.to }}) × {{ item.rate }}%/ngày =
{{ $numtoString(item.penaltyThisPeriod) }}
</p> </p>
<p class="is-size-6 mt-3"> <p class="is-size-6 mt-3">
<strong>Tổng lãi tích lũy đến {{ formatDate(item.entry.date) }}: </strong> <strong
<span :class="item.penaltyAccumulated > 0 ? 'has-text-danger' : 'has-text-grey'"> >Tổng lãi tích lũy đến
{{ formatDate(item.entry.date) }}:
</strong>
<span
:class="
item.penaltyAccumulated > 0
? 'has-text-danger'
: 'has-text-grey'
"
>
{{ $numtoString(item.penaltyAccumulated) }} {{ $numtoString(item.penaltyAccumulated) }}
</span> </span>
</p> </p>
@@ -201,20 +332,36 @@
</div> </div>
</div> </div>
</template> </template>
</div> </div>
</div> </div>
<!-- Tiền lãi hiện tại (nếu chưa hết nợ) --> <!-- Tiền lãi hiện tại (nếu chưa hết nợ) -->
<div v-if="hasUnpaidDebt && latestPenaltyToThisEntry != paymentScheduleData.penalty_amount && processedEntries.length > 0 && latestPenaltyToThisEntry > 0" class="mt-5 box has-background-warning-light"> <div
v-if="
hasUnpaidDebt &&
latestPenaltyToThisEntry != paymentScheduleData.penalty_amount &&
processedEntries.length > 0 &&
latestPenaltyToThisEntry > 0
"
class="mt-5 box has-background-warning-light"
>
<p class="is-size-5"> <p class="is-size-5">
<strong>Lãi đến ngày thanh toán gần nhất ({{ latestEntryDate }}) :</strong> {{ $numtoString(latestPenaltyToThisEntry) }} <strong
>Lãi đến ngày thanh toán gần nhất ({{ latestEntryDate }})
:</strong
>
{{ $numtoString(latestPenaltyToThisEntry) }}
</p> </p>
<p v-if="latestAdditionalPenalty > 0" class="is-size-5 mt-2"> <p v-if="latestAdditionalPenalty > 0" class="is-size-5 mt-2">
<strong>Lãi phát sinh từ ngày {{ latestEntryDate }} đến nay:</strong> {{ $numtoString(latestAdditionalPenalty) }} <strong
>Lãi phát sinh từ ngày {{ latestEntryDate }} đến nay:</strong
>
{{ $numtoString(latestAdditionalPenalty) }}
</p> </p>
<p class="is-size-5 has-text-weight-bold mt-3"> <p class="is-size-5 has-text-weight-bold mt-3">
Tổng lãi hiện tại: {{ $numtoString(latestPenaltyToThisEntry) }} + {{ $numtoString(latestAdditionalPenalty) }} = {{ $numtoString(paymentScheduleData.penalty_amount) }} Tổng lãi hiện tại: {{ $numtoString(latestPenaltyToThisEntry) }} +
{{ $numtoString(latestAdditionalPenalty) }} =
{{ $numtoString(paymentScheduleData.penalty_amount) }}
</p> </p>
</div> </div>
@@ -224,59 +371,96 @@
<div class="columns is-mobile is-multiline"> <div class="columns is-mobile is-multiline">
<div class="column is-3-tablet is-6-mobile"> <div class="column is-3-tablet is-6-mobile">
<p class="heading">Gốc đã trả</p> <p class="heading">Gốc đã trả</p>
<p class="title is-5 has-text-success">{{ $numtoString(paymentScheduleData.paid_amount) }}</p> <p class="title is-5 has-text-success">
{{ $numtoString(paymentScheduleData.paid_amount) }}
</p>
</div> </div>
<div class="column is-3-tablet is-6-mobile"> <div class="column is-3-tablet is-6-mobile">
<p class="heading">Gốc còn lại</p> <p class="heading">Gốc còn lại</p>
<p class="title is-5" <p
:class="paymentScheduleData.amount_remain > 0 ? 'has-text-danger' : 'has-text-success'"> class="title is-5"
:class="
paymentScheduleData.amount_remain > 0
? 'has-text-danger'
: 'has-text-success'
"
>
{{ $numtoString(paymentScheduleData.amount_remain) }} {{ $numtoString(paymentScheduleData.amount_remain) }}
</p> </p>
</div> </div>
<div class="column is-3-tablet is-6-mobile"> <div class="column is-3-tablet is-6-mobile">
<p class="heading">Lãi đã trả</p> <p class="heading">Lãi đã trả</p>
<p class="title is-5 has-text-success">{{ $numtoString(paymentScheduleData.penalty_paid) }}</p> <p class="title is-5 has-text-success">
{{ $numtoString(paymentScheduleData.penalty_paid) }}
</p>
</div> </div>
<div class="column is-3-tablet is-6-mobile"> <div class="column is-3-tablet is-6-mobile">
<p class="heading">Lãi còn lại</p> <p class="heading">Lãi còn lại</p>
<p class="title is-5" <p
:class="paymentScheduleData.penalty_remain > 0 ? 'has-text-danger' : 'has-text-success'"> class="title is-5"
:class="
paymentScheduleData.penalty_remain > 0
? 'has-text-danger'
: 'has-text-success'
"
>
{{ $numtoString(paymentScheduleData.penalty_remain) }} {{ $numtoString(paymentScheduleData.penalty_remain) }}
</p> </p>
</div> </div>
<div v-if="paymentScheduleData.penalty_reduce > 0" class="column is-3-tablet is-6-mobile"> <div
v-if="paymentScheduleData.penalty_reduce > 0"
class="column is-3-tablet is-6-mobile"
>
<p class="heading">Đã miễn giảm lãi</p> <p class="heading">Đã miễn giảm lãi</p>
<p class="title is-5 has-text-info">{{ $numtoString(paymentScheduleData.penalty_reduce) }}</p> <p class="title is-5 has-text-info">
{{ $numtoString(paymentScheduleData.penalty_reduce) }}
</p>
</div> </div>
<div class="column is-3-tablet is-6-mobile"> <div class="column is-3-tablet is-6-mobile">
<p class="heading">Tổng tiền phải thanh toán</p> <p class="heading">Tổng tiền phải thanh toán</p>
<p class="title is-5 has-text-primary">{{ $numtoString(paymentScheduleData.remain_amount) }}</p> <p class="title is-5 has-text-primary">
{{ $numtoString(paymentScheduleData.remain_amount) }}
</p>
</div> </div>
</div> </div>
<div v-if="isConfirmAllowed && $getEditRights()" class="field is-grouped is-grouped-left"> <div
v-if="isConfirmAllowed && $getEditRights()"
class="field is-grouped is-grouped-left"
>
<p class="control"> <p class="control">
<button class="button is-info mr-3 has-text-white" @click="handleViewEmail"> <button
class="button is-info mr-3 has-text-white"
@click="handleViewEmail"
>
<span>Gửi thông báo</span> <span>Gửi thông báo</span>
</button> </button>
<button v-if="paymentScheduleData.batch_date == null" class="button is-danger has-text-white" <button
:class="{ 'is-loading': isLoading }" @click="getPenalty" :disabled="isLoading"> v-if="paymentScheduleData.batch_date == null"
class="button is-danger has-text-white"
:class="{ 'is-loading': isLoading }"
@click="getPenalty"
:disabled="isLoading"
>
<span>Tính lãi</span> <span>Tính lãi</span>
</button> </button>
</p> </p>
<Modal @close="showModalViewEmail = undefined" v-bind="showModalViewEmail" v-if="showModalViewEmail"></Modal> <Modal
@close="showModalViewEmail = undefined"
v-bind="showModalViewEmail"
v-if="showModalViewEmail"
></Modal>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, computed, onMounted } from "vue"; import { ref, computed, onMounted } from 'vue';
import { useNuxtApp } from "#app"; import { useNuxtApp } from '#app';
import { useStore } from "@/stores/index"; import { useStore } from '@/stores/index';
import dayjs from "dayjs"; import dayjs from 'dayjs';
const props = defineProps({ const props = defineProps({
scheduleItemId: { scheduleItemId: {
@@ -285,39 +469,50 @@ const props = defineProps({
}, },
}); });
const emit = defineEmits(["close", "confirmed"]); const emit = defineEmits(['close', 'confirmed']);
const store = useStore(); const store = useStore();
const { $insertapi, $snackbar, $getEditRights, $getdata, $patchapi, $numtoString } = useNuxtApp(); const {
$insertapi,
$snackbar,
$getEditRights,
$getdata,
$patchapi,
$numtoString,
} = useNuxtApp();
const isLoading = ref(false); const isLoading = ref(false);
const loadingData = ref(true); const loadingData = ref(true);
const paymentScheduleData = ref(null); const paymentScheduleData = ref(null);
const rule = ref(null); const rule = ref(null);
const invoiceData = ref({ link: "", ref_code: "" }); const invoiceData = ref({ link: '', ref_code: '' });
const isUpdatingInvoice = ref(false); const isUpdatingInvoice = ref(false);
const showModal = ref(null); const showModal = ref(null);
const showModalViewEmail = ref(null); const showModalViewEmail = ref(null);
const isVietnamese = computed(() => store.lang === "vi"); const isVietnamese = computed(() => store.lang === 'vi');
const isOverdue = (dueDate) => { const isOverdue = (dueDate) => {
if (!dueDate) return false; if (!dueDate) return false;
return dayjs(dueDate).isBefore(dayjs(), "day"); return dayjs(dueDate).isBefore(dayjs(), 'day');
}; };
const isConfirmAllowed = computed(() => paymentScheduleData.value?.status === 1); const isConfirmAllowed = computed(
const isInvoiceAllowed = computed(() => paymentScheduleData.value?.status === 2); () => paymentScheduleData.value?.status === 1,
);
const isInvoiceAllowed = computed(
() => paymentScheduleData.value?.status === 2,
);
const formatDate = (dateString) => { const formatDate = (dateString) => {
if (!dateString) return "-"; if (!dateString) return '-';
return dayjs(dateString).format("DD/MM/YYYY"); return dayjs(dateString).format('DD/MM/YYYY');
}; };
const getEntryTypeLabel = (type) => { const getEntryTypeLabel = (type) => {
const labels = { const labels = {
PAYMENT: "Thanh toán", PAYMENT: 'Thanh toán',
REDUCTION: "Miễn giảm", REDUCTION: 'Miễn giảm',
}; };
return labels[type] || type; return labels[type] || type;
}; };
@@ -330,8 +525,8 @@ const processedEntries = computed(() => {
const relevantEntries = paymentScheduleData.value.entry.filter( const relevantEntries = paymentScheduleData.value.entry.filter(
(e) => (e) =>
(e.type === "PAYMENT" && e.penalty_added_to_entry !== undefined) || (e.type === 'PAYMENT' && e.penalty_added_to_entry !== undefined) ||
e.type === "REDUCTION" e.type === 'REDUCTION',
); );
relevantEntries.sort((a, b) => new Date(a.date) - new Date(b.date)); relevantEntries.sort((a, b) => new Date(a.date) - new Date(b.date));
@@ -348,7 +543,7 @@ const processedEntries = computed(() => {
relevantEntries.forEach((entry) => { relevantEntries.forEach((entry) => {
const entryDate = dayjs(entry.date); const entryDate = dayjs(entry.date);
if (entry.type === "PAYMENT") { if (entry.type === 'PAYMENT') {
const penaltyThisPeriod = Number(entry.penalty_added_to_entry || 0); const penaltyThisPeriod = Number(entry.penalty_added_to_entry || 0);
totalPenaltyAccumulated += penaltyThisPeriod; totalPenaltyAccumulated += penaltyThisPeriod;
@@ -365,7 +560,7 @@ const processedEntries = computed(() => {
const lastEventDate = lastDate const lastEventDate = lastDate
? dayjs(Math.max(dayjs(toDate).valueOf(), dayjs(lastDate).valueOf())) ? dayjs(Math.max(dayjs(toDate).valueOf(), dayjs(lastDate).valueOf()))
: dayjs(toDate); : dayjs(toDate);
const days = Math.max(0, entryDate.diff(lastEventDate, "day")); const days = Math.max(0, entryDate.diff(lastEventDate, 'day'));
lastDate = entry.date; // cập nhật sau khi đã tính lastDate = entry.date; // cập nhật sau khi đã tính
@@ -373,19 +568,24 @@ const processedEntries = computed(() => {
entry, entry,
isReduction: false, isReduction: false,
principalRemain: Number(entry.amount_remain_after_allocation || 0), principalRemain: Number(entry.amount_remain_after_allocation || 0),
principalBefore: Number(entry.amount_remain_after_allocation || 0) + principalPaid, principalBefore:
Number(entry.amount_remain_after_allocation || 0) + principalPaid,
penaltyAccumulated: totalPenaltyAccumulated, penaltyAccumulated: totalPenaltyAccumulated,
penaltyThisPeriod, penaltyThisPeriod,
penaltyRemain: Math.max(0, totalPenaltyAccumulated - totalPenaltyPaid - totalPenaltyReduce), penaltyRemain: Math.max(
0,
totalPenaltyAccumulated - totalPenaltyPaid - totalPenaltyReduce,
),
rate: Number(entry.DAILY_PENALTY_RATE || 0) * 100, rate: Number(entry.DAILY_PENALTY_RATE || 0) * 100,
totalDebt: Number(entry.amount_remain_after_allocation || 0) + penaltyThisPeriod, totalDebt:
Number(entry.amount_remain_after_allocation || 0) + penaltyThisPeriod,
penaltyDetail: { penaltyDetail: {
from: lastEventDate.format("DD/MM/YYYY"), from: lastEventDate.format('DD/MM/YYYY'),
to: entryDate.subtract(1, "day").format("DD/MM/YYYY"), to: entryDate.subtract(1, 'day').format('DD/MM/YYYY'),
days, days,
}, },
}); });
} else if (entry.type === "REDUCTION") { } else if (entry.type === 'REDUCTION') {
const reduceAmount = Number(entry.amount || 0); const reduceAmount = Number(entry.amount || 0);
totalPenaltyReduce += reduceAmount; totalPenaltyReduce += reduceAmount;
// REDUCTION không dịch chuyển lastDate // REDUCTION không dịch chuyển lastDate
@@ -394,7 +594,10 @@ const processedEntries = computed(() => {
entry, entry,
isReduction: true, isReduction: true,
reduceAmount, reduceAmount,
penaltyRemain: Math.max(0, totalPenaltyAccumulated - totalPenaltyPaid - totalPenaltyReduce), penaltyRemain: Math.max(
0,
totalPenaltyAccumulated - totalPenaltyPaid - totalPenaltyReduce,
),
}); });
} }
}); });
@@ -406,7 +609,7 @@ const processedEntries = computed(() => {
const latestEntry = computed(() => { const latestEntry = computed(() => {
if (!paymentScheduleData.value?.entry?.length) return null; if (!paymentScheduleData.value?.entry?.length) return null;
const paymentEntries = paymentScheduleData.value.entry.filter( const paymentEntries = paymentScheduleData.value.entry.filter(
(e) => e.type === "PAYMENT" && e.penalty_to_this_entry !== undefined (e) => e.type === 'PAYMENT' && e.penalty_to_this_entry !== undefined,
); );
if (paymentEntries.length === 0) return null; if (paymentEntries.length === 0) return null;
return paymentEntries[paymentEntries.length - 1]; return paymentEntries[paymentEntries.length - 1];
@@ -419,39 +622,59 @@ const hasUnpaidDebt = computed(() => {
); );
}); });
const latestPenaltyToThisEntry = computed(() => latestEntry.value?.penalty_to_this_entry || 0); const latestPenaltyToThisEntry = computed(
() => latestEntry.value?.penalty_to_this_entry || 0,
);
const latestAdditionalPenalty = computed(() => { const latestAdditionalPenalty = computed(() => {
return paymentScheduleData.value?.penalty_amount - (latestEntry.value?.penalty_to_this_entry || 0) || 0; return (
paymentScheduleData.value?.penalty_amount -
(latestEntry.value?.penalty_to_this_entry || 0) || 0
);
}); });
const latestTotalPenalty = computed(() => latestPenaltyToThisEntry.value + latestAdditionalPenalty.value); const latestTotalPenalty = computed(
() => latestPenaltyToThisEntry.value + latestAdditionalPenalty.value,
);
const latestEntryCode = computed(() => latestEntry.value?.code || "-"); const latestEntryCode = computed(() => latestEntry.value?.code || '-');
const latestEntryDate = computed(() => { const latestEntryDate = computed(() => {
return latestEntry.value?.date ? formatDate(latestEntry.value.date) : "-"; return latestEntry.value?.date ? formatDate(latestEntry.value.date) : '-';
}); });
const fetchPaymentScheduleData = async () => { const fetchPaymentScheduleData = async () => {
loadingData.value = true; loadingData.value = true;
try { try {
const data = await $getdata("payment_schedule", { id: props.scheduleItemId }, undefined, true); const data = await $getdata(
const ruleData = await $getdata("bizsetting", { code: "rule" }, undefined, true); 'payment_schedule',
{ id: props.scheduleItemId },
undefined,
true,
);
const ruleData = await $getdata(
'payment_schedule',
{ code: 'rule' },
undefined,
true,
);
rule.value = rule.value =
ruleData?.detail === "fee-principal" ruleData?.detail === 'fee-principal'
? "Lãi phạt quá hạn trước - Gốc sau" ? 'Lãi phạt quá hạn trước - Gốc sau'
: "Tiền gốc trước - Lãi phạt quá hạn sau"; : 'Tiền gốc trước - Lãi phạt quá hạn sau';
paymentScheduleData.value = data; paymentScheduleData.value = data;
if (data?.link) invoiceData.value.link = data.link; if (data?.link) invoiceData.value.link = data.link;
if (data?.ref_code) invoiceData.value.ref_code = data.ref_code; if (data?.ref_code) invoiceData.value.ref_code = data.ref_code;
} catch (e) { } catch (e) {
console.error("Error fetching payment schedule data:", e); console.error('Error fetching payment schedule data:', e);
$snackbar( $snackbar(
isVietnamese.value ? "Không thể tải thông tin công nợ." : "Failed to load payment schedule information.", isVietnamese.value
"Lỗi", "Error" ? 'Không thể tải thông tin công nợ.'
: 'Failed to load payment schedule information.',
'Lỗi',
'Error',
); );
} finally { } finally {
loadingData.value = false; loadingData.value = false;
@@ -462,17 +685,23 @@ const getPenalty = async () => {
isLoading.value = true; isLoading.value = true;
const target_item = paymentScheduleData.value; const target_item = paymentScheduleData.value;
const workflowPayload = { const workflowPayload = {
workflow_code: "CALCULATE_LATE_PAYMENT_PENALTY", workflow_code: 'CALCULATE_LATE_PAYMENT_PENALTY',
trigger: "create", trigger: 'create',
target_item, target_item,
}; };
try { try {
const response = await $insertapi("workflow", workflowPayload, undefined, false); const response = await $insertapi(
if (response === "error" || !response?.success) throw new Error("Calculate penalty failed"); 'workflow',
$snackbar("Tính lãi thành công!", "Thành công", "Success"); workflowPayload,
undefined,
false,
);
if (response === 'error' || !response?.success)
throw new Error('Calculate penalty failed');
$snackbar('Tính lãi thành công!', 'Thành công', 'Success');
await fetchPaymentScheduleData(); await fetchPaymentScheduleData();
} catch (e) { } catch (e) {
$snackbar("Có lỗi xảy ra khi tính lãi.", "Lỗi", "Error"); $snackbar('Có lỗi xảy ra khi tính lãi.', 'Lỗi', 'Error');
} finally { } finally {
isLoading.value = false; isLoading.value = false;
} }
@@ -481,29 +710,43 @@ const getPenalty = async () => {
const handleUpdateInvoice = async () => { const handleUpdateInvoice = async () => {
if (!invoiceData.value.link || !invoiceData.value.ref_code) { if (!invoiceData.value.link || !invoiceData.value.ref_code) {
$snackbar( $snackbar(
isVietnamese.value ? "Vui lòng nhập đầy đủ link và mã xác thực" : "Please enter both link and reference code", isVietnamese.value
"Cảnh báo", "Warning" ? 'Vui lòng nhập đầy đủ link và mã xác thực'
: 'Please enter both link and reference code',
'Cảnh báo',
'Warning',
); );
return; return;
} }
isUpdatingInvoice.value = true; isUpdatingInvoice.value = true;
try { try {
const response = await $patchapi( const response = await $patchapi(
"payment_schedule", 'payment_schedule',
{ id: props.scheduleItemId, link: invoiceData.value.link, ref_code: invoiceData.value.ref_code }, {
undefined, false id: props.scheduleItemId,
link: invoiceData.value.link,
ref_code: invoiceData.value.ref_code,
},
undefined,
false,
); );
if (response === "error" || !response) throw new Error("Update failed"); if (response === 'error' || !response) throw new Error('Update failed');
$snackbar( $snackbar(
isVietnamese.value ? "Cập nhật hóa đơn thành công!" : "Invoice updated successfully!", isVietnamese.value
"Thành công", "Success" ? 'Cập nhật hóa đơn thành công!'
: 'Invoice updated successfully!',
'Thành công',
'Success',
); );
await fetchPaymentScheduleData(); await fetchPaymentScheduleData();
} catch (error) { } catch (error) {
console.error("Error updating invoice:", error); console.error('Error updating invoice:', error);
$snackbar( $snackbar(
isVietnamese.value ? "Có lỗi xảy ra khi cập nhật hóa đơn" : "Error updating invoice", isVietnamese.value
"Lỗi", "Error" ? 'Có lỗi xảy ra khi cập nhật hóa đơn'
: 'Error updating invoice',
'Lỗi',
'Error',
); );
} finally { } finally {
isUpdatingInvoice.value = false; isUpdatingInvoice.value = false;
@@ -511,30 +754,31 @@ const handleUpdateInvoice = async () => {
}; };
const resetInvoiceData = () => { const resetInvoiceData = () => {
invoiceData.value = { link: "", ref_code: "" }; invoiceData.value = { link: '', ref_code: '' };
}; };
function openEntryDetailModal(entry) { function openEntryDetailModal(entry) {
if (!entry.code) return; if (!entry.code) return;
showModal.value = { showModal.value = {
component: "accounting/InternalEntry", component: 'accounting/InternalEntry',
title: `Chi tiết bút toán: ${entry.code}`, title: `Chi tiết bút toán: ${entry.code}`,
height: "500px", height: '500px',
width: "80%", width: '80%',
vbind: { row: { code: entry.code } }, vbind: { row: { code: entry.code } },
}; };
} }
async function handleViewEmail() { async function handleViewEmail() {
const emailTemplate = await $getdata( const emailTemplate = await $getdata(
"emailtemplate", 'emailtemplate',
{ name: "Mail Thông báo đến hạn thanh toán" }, { name: 'Mail Thông báo đến hạn thanh toán' },
undefined, false undefined,
false,
); );
showModalViewEmail.value = { showModalViewEmail.value = {
component: "marketing/email/viewEmail/ViewEmail", component: 'marketing/email/viewEmail/ViewEmail',
title: "Xem trước nội dung nhắc thanh toán", title: 'Xem trước nội dung nhắc thanh toán',
width: "60%", width: '60%',
vbind: { vbind: {
idEmailTemplate: emailTemplate[0]?.id || null, idEmailTemplate: emailTemplate[0]?.id || null,
scheduleItemId: props.scheduleItemId, scheduleItemId: props.scheduleItemId,
@@ -543,8 +787,12 @@ async function handleViewEmail() {
}; };
} }
const handleModalClose = () => { showModal.value = null; }; const handleModalClose = () => {
const handleConfirmDelete = () => { showModal.value = null; }; showModal.value = null;
};
const handleConfirmDelete = () => {
showModal.value = null;
};
onMounted(() => { onMounted(() => {
fetchPaymentScheduleData(); fetchPaymentScheduleData();

View File

@@ -3,13 +3,15 @@
<div class="container is-fluid px-4"> <div class="container is-fluid px-4">
<div> <div>
<p> <p>
Bạn chắc chắn muốn xóa lịch công nợ thời gian: {{ detail.time }} ngày - mẫu: [{{ Bạn chắc chắn muốn xóa lịch công nợ thời gian:
detail.name?.toUpperCase() {{ detail.time }} ngày - mẫu: [{{ detail.name?.toUpperCase() }}]
}}] không? không?
</p> </p>
<div class="action mt-3"> <div class="action mt-3">
<button class="button is-light" @click="handleCancel">Hủy</button> <button class="button is-light" @click="handleCancel">Hủy</button>
<button class="button is-primary" @click="handleDeleteCart">Đồng ý</button> <button class="button is-primary" @click="handleDeleteCart">
Đồng ý
</button>
</div> </div>
</div> </div>
</div> </div>
@@ -19,7 +21,7 @@
<script setup> <script setup>
const { $snackbar, $deleteapi } = useNuxtApp(); const { $snackbar, $deleteapi } = useNuxtApp();
const emit = defineEmits(["close"]); const emit = defineEmits(['close']);
const props = defineProps({ const props = defineProps({
row: Object, row: Object,
}); });
@@ -27,15 +29,15 @@ const props = defineProps({
const detail = JSON.parse(props.row?.detail || null); const detail = JSON.parse(props.row?.detail || null);
const handleDeleteCart = async () => { const handleDeleteCart = async () => {
const res = await $deleteapi("bizsetting", props.row.id); const res = await $deleteapi('user', props.row.id);
if (res) { if (res) {
emit("close"); emit('close');
$snackbar("Xóa lịch công nợ thành công"); $snackbar('Xóa lịch công nợ thành công');
} }
}; };
const handleCancel = () => { const handleCancel = () => {
emit("close"); emit('close');
}; };
</script> </script>

View File

@@ -4,7 +4,6 @@
class="has-background-white" class="has-background-white"
data-theme="light" data-theme="light"
lang="vi" lang="vi"
v-if="authorized"
> >
<slot /> <slot />
@@ -24,7 +23,7 @@ import { useRoute } from 'vue-router';
import SnackBar from '@/components/snackbar/SnackBar.vue'; import SnackBar from '@/components/snackbar/SnackBar.vue';
import Modal from '@/components/Modal.vue'; import Modal from '@/components/Modal.vue';
const route = useRoute(); const route = useRoute();
const { $getdata, $requestLogin, $store } = useNuxtApp(); const { $getdata, $store } = useNuxtApp();
var authorized = ref(false); var authorized = ref(false);
const snackbar = ref(undefined); const snackbar = ref(undefined);
const showmodal = ref(undefined); const showmodal = ref(undefined);
@@ -46,6 +45,7 @@ function getViewport() {
$store.commit('viewport', viewport); $store.commit('viewport', viewport);
} }
async function checkRedirect() { async function checkRedirect() {
console.log('checkRedirect');
if (route.query.username && route.query.token) { if (route.query.username && route.query.token) {
let row = await $getdata( let row = await $getdata(
'user', 'user',
@@ -57,12 +57,14 @@ async function checkRedirect() {
}, },
true, true,
); );
if (row === 'error' || row === undefined) $requestLogin(); if (row === 'error' || row === undefined) return;
else { else {
row.token = route.query.token; row.token = route.query.token;
$store.commit('login', row); $store.commit('login', row);
} }
} else if (!$store.login) return $requestLogin(); } else if (!$store.login) {
return;
}
await checkLogin(); await checkLogin();
} }
async function checkLogin() { async function checkLogin() {
@@ -89,9 +91,9 @@ async function checkLogin() {
undefined, undefined,
true, true,
); );
if (authtoken ? authtoken.expiry : true) return $requestLogin(); if (authtoken ? authtoken.expiry : true) return;
authorized.value = true; authorized.value = true;
} else $requestLogin(); }
} }
onMounted(() => { onMounted(() => {
checkRedirect(); checkRedirect();

View File

@@ -603,13 +603,6 @@ export default defineNuxtPlugin(() => {
url_detail: "data-detail/Menu_Choice/", url_detail: "data-detail/Menu_Choice/",
params: {}, params: {},
}, },
{
name: "moneyunit",
commit: "moneyunit",
url: "data/Money_Unit/",
url_detail: "data-detail/Money_Unit/",
params: {},
},
{ {
name: "legaltype", name: "legaltype",
commit: "legaltype", commit: "legaltype",
@@ -847,6 +840,7 @@ export default defineNuxtPlugin(() => {
store.commit("login", undefined); store.commit("login", undefined);
store.commit("layersetting", undefined); store.commit("layersetting", undefined);
store.commit("lastlegendfiltertab", "Giỏ hàng"); store.commit("lastlegendfiltertab", "Giỏ hàng");
console.log('requestLogin: redirect to login')
window.location.href = `https://${mode === "dev" ? "dev." : ""}login.utopia.com.vn/signin?module=${module}&link=${window.location.origin}`; window.location.href = `https://${mode === "dev" ? "dev." : ""}login.utopia.com.vn/signin?module=${module}&link=${window.location.origin}`;
}; };
@@ -876,6 +870,7 @@ export default defineNuxtPlugin(() => {
// get data // get data
const getapi = async function (list) { const getapi = async function (list) {
console.trace('getapi')
try { try {
let arr = list.map((v) => { let arr = list.map((v) => {
let found = apis.find((api) => api.name === v.name); let found = apis.find((api) => api.name === v.name);
@@ -898,7 +893,7 @@ export default defineNuxtPlugin(() => {
}); });
return list; return list;
} catch (err) { } catch (err) {
console.log(err); console.error(err);
return "error"; return "error";
} }
}; };

View File

@@ -2,7 +2,7 @@ export default defineNuxtPlugin(async (nuxtApp) => {
const { $getapi, $readyapi } = useNuxtApp() const { $getapi, $readyapi } = useNuxtApp()
let connlist = $readyapi(['moneyunit', 'datatype', 'filterchoice', 'colorchoice', 'textalign', 'placement', 'colorscheme', let connlist = $readyapi(['moneyunit', 'datatype', 'filterchoice', 'colorchoice', 'textalign', 'placement', 'colorscheme',
'filtertype', 'sorttype', 'tablesetting', 'settingchoice', 'sharechoice', 'menuchoice', 'settingtype', 'settingclass', 'filtertype', 'sorttype', 'tablesetting', 'settingchoice', 'sharechoice', 'menuchoice', 'settingtype', 'settingclass',
'common', 'sex', 'legaltype', 'cart']) 'sex', 'legaltype', 'cart'])
let filter = connlist.filter(v=>!v.ready) let filter = connlist.filter(v=>!v.ready)
if(filter.length>0) await $getapi(filter) if(filter.length>0) await $getapi(filter)
}) })