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

@@ -3,7 +3,11 @@
<div v-if="loadingData" class="has-text-centered py-5">
<SvgIcon v-bind="{ name: 'loading.svg', type: 'primary', size: 18 }" />
<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>
</div>
@@ -12,95 +16,171 @@
<!-- Thông tin bản -->
<div class="columns is-multiline is-mobile">
<div class="column is-3">
<strong>{{ isVietnamese ? "Mã:" : "Schedule Code:" }}</strong>
<p>{{ paymentScheduleData.code || "-" }}</p>
<strong>{{ isVietnamese ? 'Mã:' : 'Schedule Code:' }}</strong>
<p>{{ paymentScheduleData.code || '-' }}</p>
</div>
<div class="column is-3">
<strong>{{ isVietnamese ? "Trạng thái:" : "Status:" }}</strong>
<p :class="{
'has-text-success': paymentScheduleData.status__name === 'Đã xác nhận' || paymentScheduleData.status__name === 'Paid',
'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 || "-" }}
<strong>{{ isVietnamese ? 'Trạng thái:' : 'Status:' }}</strong>
<p
:class="{
'has-text-success':
paymentScheduleData.status__name === 'Đã xác nhận' ||
paymentScheduleData.status__name === 'Paid',
'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>
</div>
<div class="column is-3">
<strong>{{ isVietnamese ? "Loại thanh toán:" : "Payment Type:" }}</strong>
<p>{{ paymentScheduleData.type__name || "-" }}</p>
<strong>{{
isVietnamese ? 'Loại thanh toán:' : 'Payment Type:'
}}</strong>
<p>{{ paymentScheduleData.type__name || '-' }}</p>
</div>
<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>
</div>
<div class="column is-3">
<strong>{{ 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>
<strong>{{
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 class="column is-3">
<strong>{{ 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>
<strong>{{
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 class="column is-3">
<strong>{{ isVietnamese ? "Số tiền gốc còn lại:" : "Remaining Principal:" }}</strong>
<p class="has-text-weight-bold"
:class="paymentScheduleData.amount_remain > 0 ? 'has-text-danger' : 'has-text-success'">
<strong>{{
isVietnamese ? 'Số tiền gốc còn lại:' : 'Remaining Principal:'
}}</strong>
<p
class="has-text-weight-bold"
:class="
paymentScheduleData.amount_remain > 0
? 'has-text-danger'
: 'has-text-success'
"
>
{{ $numtoString(paymentScheduleData.amount_remain) }}
</p>
</div>
<div class="column is-3">
<strong>{{ isVietnamese ? "Ngày đến hạn:" : "Due Date:" }}</strong>
<p :class="{ 'has-text-danger': isOverdue(paymentScheduleData.to_date && paymentScheduleData.ovd_days > 0) }">
<strong>{{ isVietnamese ? 'Ngày đến hạn:' : 'Due Date:' }}</strong>
<p
:class="{
'has-text-danger': isOverdue(
paymentScheduleData.to_date &&
paymentScheduleData.ovd_days > 0,
),
}"
>
{{ 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)
</span>
</p>
</div>
<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">
{{ paymentScheduleData.penalty_amount > 0 ? $numtoString(paymentScheduleData.penalty_amount) : "-" }}
{{
paymentScheduleData.penalty_amount > 0
? $numtoString(paymentScheduleData.penalty_amount)
: '-'
}}
</p>
</div>
<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">
{{ paymentScheduleData.penalty_paid > 0 ? $numtoString(paymentScheduleData.penalty_paid) : "-" }}
{{
paymentScheduleData.penalty_paid > 0
? $numtoString(paymentScheduleData.penalty_paid)
: '-'
}}
</p>
</div>
<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">
{{ paymentScheduleData.penalty_remain > 0 ? $numtoString(paymentScheduleData.penalty_remain) : "-" }}
{{
paymentScheduleData.penalty_remain > 0
? $numtoString(paymentScheduleData.penalty_remain)
: '-'
}}
</p>
</div>
<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">
{{ paymentScheduleData.penalty_reduce > 0 ? $numtoString(paymentScheduleData.penalty_reduce) : "-" }}
{{
paymentScheduleData.penalty_reduce > 0
? $numtoString(paymentScheduleData.penalty_reduce)
: '-'
}}
</p>
</div>
<div class="column is-3">
<strong>{{ 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>
<strong>{{
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 class="column is-3">
<strong>{{ isVietnamese ? "Ghi chú:" : "Note:" }}</strong>
<p class="is-size-6">{{ paymentScheduleData.detail?.note || "-" }}</p>
<strong>{{ isVietnamese ? 'Ghi chú:' : 'Note:' }}</strong>
<p class="is-size-6">
{{ paymentScheduleData.detail?.note || '-' }}
</p>
</div>
</div>
<hr />
<!-- Timeline lịch sử -->
<div v-if="processedEntries.length > 0" 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">
<div
v-if="processedEntries.length > 0"
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 ===================== -->
<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">
{{ formatDate(item.entry.date) }}
</p>
@@ -112,23 +192,40 @@
<div class="is-flex-grow-1">
<p class="is-size-5 has-text-weight-bold has-text-info mb-2">
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 }}
</span>
</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="column is-6-mobile">
<span class="has-text-grey-light">Số tiền miễn giảm:</span><br />
<span class="has-text-info has-text-weight-semibold is-size-5">
<span class="has-text-grey-light">Số tiền miễn giảm:</span
><br />
<span
class="has-text-info has-text-weight-semibold is-size-5"
>
{{ $numtoString(item.reduceAmount) }}
</span>
</div>
<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-weight-semibold is-size-5"
:class="item.penaltyRemain > 0 ? 'has-text-danger' : 'has-text-success'">
<span class="has-text-grey-light"
>Lãi còn lại sau miễn giảm:</span
><br />
<span
class="has-text-weight-semibold is-size-5"
:class="
item.penaltyRemain > 0
? 'has-text-danger'
: 'has-text-success'
"
>
{{ $numtoString(item.penaltyRemain) }}
</span>
</div>
@@ -139,7 +236,7 @@
<!-- ===================== PAYMENT ===================== -->
<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">
{{ formatDate(item.entry.date) }}
</p>
@@ -151,16 +248,25 @@
<div class="is-flex-grow-1">
<p class="is-size-5 has-text-weight-bold has-text-dark mb-2">
{{ 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 }}
</span>
</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="column is-6-mobile">
<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) }}
</span>
<span v-else class="has-text-grey-light">-</span>
@@ -168,16 +274,26 @@
<div class="column is-6-mobile">
<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) }}
</span>
<span v-else class="has-text-grey-light">-</span>
</div>
<div class="column is-6-mobile">
<span class="has-text-grey-light">Gốc còn lại:</span><br />
<strong :class="item.principalRemain > 0 ? 'has-text-danger' : 'has-text-success'"
class="is-size-5">
<span class="has-text-grey-light">Gốc còn lại:</span
><br />
<strong
:class="
item.principalRemain > 0
? 'has-text-danger'
: 'has-text-success'
"
class="is-size-5"
>
{{ $numtoString(item.principalRemain) }}
</strong>
</div>
@@ -185,15 +301,30 @@
<!-- Chi tiết lãi phát sinh -->
<div class="">
<p v-if="item.penaltyThisPeriod > 0" class="is-size-6 has-text-grey">
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
v-if="item.penaltyThisPeriod > 0"
class="is-size-6 has-text-grey"
>
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 class="is-size-6 mt-3">
<strong>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'">
<strong
>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) }}
</span>
</p>
@@ -201,20 +332,36 @@
</div>
</div>
</template>
</div>
</div>
<!-- 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">
<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 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 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>
</div>
@@ -224,59 +371,96 @@
<div class="columns is-mobile is-multiline">
<div class="column is-3-tablet is-6-mobile">
<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 class="column is-3-tablet is-6-mobile">
<p class="heading">Gốc còn lại</p>
<p class="title is-5"
:class="paymentScheduleData.amount_remain > 0 ? 'has-text-danger' : 'has-text-success'">
<p
class="title is-5"
:class="
paymentScheduleData.amount_remain > 0
? 'has-text-danger'
: 'has-text-success'
"
>
{{ $numtoString(paymentScheduleData.amount_remain) }}
</p>
</div>
<div class="column is-3-tablet is-6-mobile">
<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 class="column is-3-tablet is-6-mobile">
<p class="heading">Lãi còn lại</p>
<p class="title is-5"
:class="paymentScheduleData.penalty_remain > 0 ? 'has-text-danger' : 'has-text-success'">
<p
class="title is-5"
:class="
paymentScheduleData.penalty_remain > 0
? 'has-text-danger'
: 'has-text-success'
"
>
{{ $numtoString(paymentScheduleData.penalty_remain) }}
</p>
</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="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 class="column is-3-tablet is-6-mobile">
<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 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">
<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>
</button>
<button v-if="paymentScheduleData.batch_date == null" class="button is-danger has-text-white"
:class="{ 'is-loading': isLoading }" @click="getPenalty" :disabled="isLoading">
<button
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>
</button>
</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>
</template>
<script setup>
import { ref, computed, onMounted } from "vue";
import { useNuxtApp } from "#app";
import { useStore } from "@/stores/index";
import dayjs from "dayjs";
import { ref, computed, onMounted } from 'vue';
import { useNuxtApp } from '#app';
import { useStore } from '@/stores/index';
import dayjs from 'dayjs';
const props = defineProps({
scheduleItemId: {
@@ -285,39 +469,50 @@ const props = defineProps({
},
});
const emit = defineEmits(["close", "confirmed"]);
const emit = defineEmits(['close', 'confirmed']);
const store = useStore();
const { $insertapi, $snackbar, $getEditRights, $getdata, $patchapi, $numtoString } = useNuxtApp();
const {
$insertapi,
$snackbar,
$getEditRights,
$getdata,
$patchapi,
$numtoString,
} = useNuxtApp();
const isLoading = ref(false);
const loadingData = ref(true);
const paymentScheduleData = ref(null);
const rule = ref(null);
const invoiceData = ref({ link: "", ref_code: "" });
const invoiceData = ref({ link: '', ref_code: '' });
const isUpdatingInvoice = ref(false);
const showModal = ref(null);
const showModalViewEmail = ref(null);
const isVietnamese = computed(() => store.lang === "vi");
const isVietnamese = computed(() => store.lang === 'vi');
const isOverdue = (dueDate) => {
if (!dueDate) return false;
return dayjs(dueDate).isBefore(dayjs(), "day");
return dayjs(dueDate).isBefore(dayjs(), 'day');
};
const isConfirmAllowed = computed(() => paymentScheduleData.value?.status === 1);
const isInvoiceAllowed = computed(() => paymentScheduleData.value?.status === 2);
const isConfirmAllowed = computed(
() => paymentScheduleData.value?.status === 1,
);
const isInvoiceAllowed = computed(
() => paymentScheduleData.value?.status === 2,
);
const formatDate = (dateString) => {
if (!dateString) return "-";
return dayjs(dateString).format("DD/MM/YYYY");
if (!dateString) return '-';
return dayjs(dateString).format('DD/MM/YYYY');
};
const getEntryTypeLabel = (type) => {
const labels = {
PAYMENT: "Thanh toán",
REDUCTION: "Miễn giảm",
PAYMENT: 'Thanh toán',
REDUCTION: 'Miễn giảm',
};
return labels[type] || type;
};
@@ -330,33 +525,33 @@ const processedEntries = computed(() => {
const relevantEntries = paymentScheduleData.value.entry.filter(
(e) =>
(e.type === "PAYMENT" && e.penalty_added_to_entry !== undefined) ||
e.type === "REDUCTION"
(e.type === 'PAYMENT' && e.penalty_added_to_entry !== undefined) ||
e.type === 'REDUCTION',
);
relevantEntries.sort((a, b) => new Date(a.date) - new Date(b.date));
let currentPrincipal = Number(paymentScheduleData.value.amount || 0);
let currentPrincipal = Number(paymentScheduleData.value.amount || 0);
let totalPenaltyAccumulated = 0;
let totalPenaltyPaid = 0;
let totalPenaltyReduce = 0;
let lastDate = null; // null = chưa có lần trả nào
const toDate = paymentScheduleData.value.to_date;
let totalPenaltyPaid = 0;
let totalPenaltyReduce = 0;
let lastDate = null; // null = chưa có lần trả nào
const toDate = paymentScheduleData.value.to_date;
const result = [];
relevantEntries.forEach((entry) => {
const entryDate = dayjs(entry.date);
if (entry.type === "PAYMENT") {
if (entry.type === 'PAYMENT') {
const penaltyThisPeriod = Number(entry.penalty_added_to_entry || 0);
totalPenaltyAccumulated += penaltyThisPeriod;
const principalPaid = Number(entry.principal || 0);
const penaltyPaid = Number(entry.penalty || 0);
currentPrincipal -= principalPaid;
const penaltyPaid = Number(entry.penalty || 0);
currentPrincipal -= principalPaid;
if (currentPrincipal < 0) currentPrincipal = 0;
totalPenaltyPaid += penaltyPaid;
totalPenaltyPaid += penaltyPaid;
// last_event = max(to_date, lastDate) — khớp với logic backend
// Nếu chưa có lần trả nào: dùng to_date
@@ -365,36 +560,44 @@ const processedEntries = computed(() => {
const lastEventDate = lastDate
? dayjs(Math.max(dayjs(toDate).valueOf(), dayjs(lastDate).valueOf()))
: 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
result.push({
entry,
isReduction: false,
principalRemain: Number(entry.amount_remain_after_allocation || 0),
principalBefore: Number(entry.amount_remain_after_allocation || 0) + principalPaid,
isReduction: false,
principalRemain: Number(entry.amount_remain_after_allocation || 0),
principalBefore:
Number(entry.amount_remain_after_allocation || 0) + principalPaid,
penaltyAccumulated: totalPenaltyAccumulated,
penaltyThisPeriod,
penaltyRemain: Math.max(0, totalPenaltyAccumulated - totalPenaltyPaid - totalPenaltyReduce),
rate: Number(entry.DAILY_PENALTY_RATE || 0) * 100,
totalDebt: Number(entry.amount_remain_after_allocation || 0) + penaltyThisPeriod,
penaltyRemain: Math.max(
0,
totalPenaltyAccumulated - totalPenaltyPaid - totalPenaltyReduce,
),
rate: Number(entry.DAILY_PENALTY_RATE || 0) * 100,
totalDebt:
Number(entry.amount_remain_after_allocation || 0) + penaltyThisPeriod,
penaltyDetail: {
from: lastEventDate.format("DD/MM/YYYY"),
to: entryDate.subtract(1, "day").format("DD/MM/YYYY"),
from: lastEventDate.format('DD/MM/YYYY'),
to: entryDate.subtract(1, 'day').format('DD/MM/YYYY'),
days,
},
});
} else if (entry.type === "REDUCTION") {
const reduceAmount = Number(entry.amount || 0);
totalPenaltyReduce += reduceAmount;
} else if (entry.type === 'REDUCTION') {
const reduceAmount = Number(entry.amount || 0);
totalPenaltyReduce += reduceAmount;
// REDUCTION không dịch chuyển lastDate
result.push({
entry,
isReduction: true,
isReduction: true,
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(() => {
if (!paymentScheduleData.value?.entry?.length) return null;
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;
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(() => {
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(() => {
return latestEntry.value?.date ? formatDate(latestEntry.value.date) : "-";
return latestEntry.value?.date ? formatDate(latestEntry.value.date) : '-';
});
const fetchPaymentScheduleData = async () => {
loadingData.value = true;
try {
const data = await $getdata("payment_schedule", { id: props.scheduleItemId }, undefined, true);
const ruleData = await $getdata("bizsetting", { code: "rule" }, undefined, true);
const data = await $getdata(
'payment_schedule',
{ id: props.scheduleItemId },
undefined,
true,
);
const ruleData = await $getdata(
'payment_schedule',
{ code: 'rule' },
undefined,
true,
);
rule.value =
ruleData?.detail === "fee-principal"
? "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";
ruleData?.detail === 'fee-principal'
? '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';
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;
} catch (e) {
console.error("Error fetching payment schedule data:", e);
console.error('Error fetching payment schedule data:', e);
$snackbar(
isVietnamese.value ? "Không thể tải thông tin công nợ." : "Failed to load payment schedule information.",
"Lỗi", "Error"
isVietnamese.value
? 'Không thể tải thông tin công nợ.'
: 'Failed to load payment schedule information.',
'Lỗi',
'Error',
);
} finally {
loadingData.value = false;
@@ -462,17 +685,23 @@ const getPenalty = async () => {
isLoading.value = true;
const target_item = paymentScheduleData.value;
const workflowPayload = {
workflow_code: "CALCULATE_LATE_PAYMENT_PENALTY",
trigger: "create",
workflow_code: 'CALCULATE_LATE_PAYMENT_PENALTY',
trigger: 'create',
target_item,
};
try {
const response = await $insertapi("workflow", 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");
const response = await $insertapi(
'workflow',
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();
} 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 {
isLoading.value = false;
}
@@ -481,29 +710,43 @@ const getPenalty = async () => {
const handleUpdateInvoice = async () => {
if (!invoiceData.value.link || !invoiceData.value.ref_code) {
$snackbar(
isVietnamese.value ? "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"
isVietnamese.value
? '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;
}
isUpdatingInvoice.value = true;
try {
const response = await $patchapi(
"payment_schedule",
{ id: props.scheduleItemId, link: invoiceData.value.link, ref_code: invoiceData.value.ref_code },
undefined, false
'payment_schedule',
{
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(
isVietnamese.value ? "Cập nhật hóa đơn thành công!" : "Invoice updated successfully!",
"Thành công", "Success"
isVietnamese.value
? 'Cập nhật hóa đơn thành công!'
: 'Invoice updated successfully!',
'Thành công',
'Success',
);
await fetchPaymentScheduleData();
} catch (error) {
console.error("Error updating invoice:", error);
console.error('Error updating invoice:', error);
$snackbar(
isVietnamese.value ? "Có lỗi xảy ra khi cập nhật hóa đơn" : "Error updating invoice",
"Lỗi", "Error"
isVietnamese.value
? 'Có lỗi xảy ra khi cập nhật hóa đơn'
: 'Error updating invoice',
'Lỗi',
'Error',
);
} finally {
isUpdatingInvoice.value = false;
@@ -511,30 +754,31 @@ const handleUpdateInvoice = async () => {
};
const resetInvoiceData = () => {
invoiceData.value = { link: "", ref_code: "" };
invoiceData.value = { link: '', ref_code: '' };
};
function openEntryDetailModal(entry) {
if (!entry.code) return;
showModal.value = {
component: "accounting/InternalEntry",
component: 'accounting/InternalEntry',
title: `Chi tiết bút toán: ${entry.code}`,
height: "500px",
width: "80%",
height: '500px',
width: '80%',
vbind: { row: { code: entry.code } },
};
}
async function handleViewEmail() {
const emailTemplate = await $getdata(
"emailtemplate",
{ name: "Mail Thông báo đến hạn thanh toán" },
undefined, false
'emailtemplate',
{ name: 'Mail Thông báo đến hạn thanh toán' },
undefined,
false,
);
showModalViewEmail.value = {
component: "marketing/email/viewEmail/ViewEmail",
title: "Xem trước nội dung nhắc thanh toán",
width: "60%",
component: 'marketing/email/viewEmail/ViewEmail',
title: 'Xem trước nội dung nhắc thanh toán',
width: '60%',
vbind: {
idEmailTemplate: emailTemplate[0]?.id || null,
scheduleItemId: props.scheduleItemId,
@@ -543,10 +787,14 @@ async function handleViewEmail() {
};
}
const handleModalClose = () => { showModal.value = null; };
const handleConfirmDelete = () => { showModal.value = null; };
const handleModalClose = () => {
showModal.value = null;
};
const handleConfirmDelete = () => {
showModal.value = null;
};
onMounted(() => {
fetchPaymentScheduleData();
});
</script>
</script>