changes
This commit is contained in:
@@ -1,362 +0,0 @@
|
||||
<template>
|
||||
<div class="">
|
||||
<p class="has-text-centered title is-4 has-text-danger mb-4">
|
||||
Vui lòng kiểm tra kỹ thông tin trước khi thay đổi ngày đến hạn
|
||||
</p>
|
||||
|
||||
<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..." }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="paymentScheduleData">
|
||||
<div class="content">
|
||||
<div class="columns is-multiline is-mobile">
|
||||
<div class="column is-4">
|
||||
<strong>{{ isVietnamese ? "Mã:" : "Schedule Code:" }}</strong>
|
||||
<p>{{ paymentScheduleData.code || "-" }}</p>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<strong>{{ isVietnamese ? "Trạng thái:" : "Status:" }}</strong>
|
||||
<p
|
||||
:class="{
|
||||
'has-text-success': paymentScheduleData.status__name === 'Đã xác nhận',
|
||||
'has-text-warning': paymentScheduleData.status__name === 'Chưa xác nhận',
|
||||
}"
|
||||
>
|
||||
{{ paymentScheduleData.status__name || "-" }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<strong>{{ isVietnamese ? "Loại thanh toán:" : "Payment Type:" }}</strong>
|
||||
<p>{{ paymentScheduleData.type__name || "-" }}</p>
|
||||
</div>
|
||||
|
||||
<div class="column is-4">
|
||||
<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-4">
|
||||
<strong>{{ isVietnamese ? "Ngày đến hạn hiện tại:" : "Current Due Date:" }}</strong>
|
||||
<p class="has-text-weight-bold">
|
||||
{{ formatDate(paymentScheduleData.to_date) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<strong>{{ isVietnamese ? "Ngày tính lãi:" : "Penalty Date:" }}</strong>
|
||||
<p :class="paymentScheduleData.batch_date ? 'has-text-danger' : ''">
|
||||
{{ formatDate(paymentScheduleData.batch_date) || "Chưa tính lãi" }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<p class="title is-6">
|
||||
{{ isVietnamese ? "Thông tin Giao dịch liên quan" : "Related Transaction Information" }}
|
||||
</p>
|
||||
<div class="columns is-multiline is-mobile">
|
||||
<div class="column is-4">
|
||||
<strong>{{ isVietnamese ? "Mã giao dịch:" : "Transaction Code:" }}</strong>
|
||||
<p>
|
||||
{{ paymentScheduleData.txn_detail__transaction__code || "-" }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<strong>{{ isVietnamese ? "Khách hàng:" : "Customer:" }}</strong>
|
||||
<p>
|
||||
{{ paymentScheduleData.txn_detail__transaction__customer__fullname || "-" }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="column is-4">
|
||||
<strong>{{ isVietnamese ? "Chính sách:" : "Policy:" }}</strong>
|
||||
<p>
|
||||
{{ paymentScheduleData.txn_detail__transaction__policy__code || "-" }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div v-if="canEditDueDate">
|
||||
<Caption
|
||||
class="mb-4"
|
||||
v-bind="{ title: 'Thay đổi ngày đến hạn', size: 20 }"
|
||||
/>
|
||||
|
||||
<div class="field">
|
||||
<label class="label has-text-weight-bold">{{ isVietnamese ? "Ngày đến hạn mới" : "New Due Date" }}</label>
|
||||
<div class="control">
|
||||
<Datepicker
|
||||
:record="dateRecord"
|
||||
attr="newDueDate"
|
||||
@date="updateDueDate"
|
||||
position="is-bottom-left"
|
||||
:mindate="minDate"
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
v-if="dateError"
|
||||
class="help is-danger"
|
||||
>
|
||||
{{ dateError }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field mt-5">
|
||||
<label class="label has-text-weight-bold">{{ isVietnamese ? "Mã xác nhận" : "Confirmation Code" }}</label>
|
||||
<div class="control">
|
||||
<div class="field has-addons">
|
||||
<div class="control is-expanded">
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
:placeholder="isVietnamese ? 'Nhập mã xác nhận' : 'Enter confirmation code'"
|
||||
v-model="userInputCaptcha"
|
||||
@keydown.enter="handleUpdate"
|
||||
/>
|
||||
</div>
|
||||
<div class="control">
|
||||
<a
|
||||
class="button is-static has-text-weight-bold has-background-grey-lighter"
|
||||
style="font-family: "Courier New", monospace; letter-spacing: 2px"
|
||||
>
|
||||
{{ captchaCode }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button
|
||||
class="button is-info is-light"
|
||||
@click="generateCaptcha"
|
||||
:title="isVietnamese ? 'Tạo mã mới' : 'Generate new code'"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'refresh.svg', type: 'primary', size: 23 }"></SvgIcon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
v-if="!isCaptchaValid && userInputCaptcha.length > 0"
|
||||
class="help is-danger"
|
||||
>
|
||||
Mã xác nhận không đúng.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="notification is-warning"
|
||||
>
|
||||
<p class="has-text-weight-bold">
|
||||
{{ isVietnamese ? "Không thể thay đổi ngày đến hạn" : "Cannot change due date" }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field is-grouped is-grouped-left mt-5">
|
||||
<p class="control">
|
||||
<button
|
||||
v-if="canEditDueDate"
|
||||
class="button is-success has-text-white"
|
||||
:class="{ 'is-loading': isLoading }"
|
||||
@click="handleUpdate"
|
||||
:disabled="!isUpdateValid || isLoading"
|
||||
>
|
||||
<span>{{ isVietnamese ? "Cập nhật ngày đến hạn" : "Update Due Date" }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="button"
|
||||
@click="emit('close')"
|
||||
>
|
||||
<span>{{ isVietnamese ? "Đóng" : "Close" }}</span>
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { useNuxtApp } from "#app";
|
||||
import { useStore } from "@/stores/index";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const props = defineProps({
|
||||
scheduleItemId: {
|
||||
type: [Number, String],
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["close", "updated"]);
|
||||
|
||||
const store = useStore();
|
||||
const { $getdata, $patchapi, $snackbar } = useNuxtApp();
|
||||
|
||||
const isLoading = ref(false);
|
||||
const loadingData = ref(true);
|
||||
const paymentScheduleData = ref(null);
|
||||
const dateRecord = ref({ newDueDate: null });
|
||||
const captchaCode = ref("");
|
||||
const userInputCaptcha = ref("");
|
||||
|
||||
const isVietnamese = computed(() => store.lang === "vi");
|
||||
|
||||
// Kiểm tra xem có thể sửa ngày đến hạn không (chưa tính lãi và chưa thanh toán)
|
||||
const canEditDueDate = computed(() => {
|
||||
const hasNoPenaltyCalculated =
|
||||
paymentScheduleData.value?.batch_date === null || paymentScheduleData.value?.batch_date === undefined;
|
||||
const isNotPaid = paymentScheduleData.value?.status !== 2;
|
||||
return hasNoPenaltyCalculated && isNotPaid;
|
||||
});
|
||||
|
||||
// Ngày tối thiểu (phải lớn hơn ngày hiện tại)
|
||||
const minDate = computed(() => {
|
||||
const currentDate = paymentScheduleData.value?.to_date;
|
||||
if (currentDate) {
|
||||
return dayjs(currentDate).add(1, "day").format("YYYY-MM-DD");
|
||||
}
|
||||
return dayjs().add(1, "day").format("YYYY-MM-DD");
|
||||
});
|
||||
|
||||
// Kiểm tra lỗi ngày
|
||||
const dateError = computed(() => {
|
||||
if (!dateRecord.value.newDueDate) return "";
|
||||
|
||||
const selectedDate = dayjs(dateRecord.value.newDueDate);
|
||||
const currentDueDate = dayjs(paymentScheduleData.value?.to_date);
|
||||
|
||||
if (selectedDate.isBefore(currentDueDate) || selectedDate.isSame(currentDueDate)) {
|
||||
return isVietnamese.value
|
||||
? "Ngày đến hạn mới phải lớn hơn ngày đến hạn hiện tại"
|
||||
: "New due date must be after current due date";
|
||||
}
|
||||
|
||||
return "";
|
||||
});
|
||||
|
||||
const updateDueDate = (date) => {
|
||||
dateRecord.value.newDueDate = date;
|
||||
};
|
||||
|
||||
const generateCaptcha = () => {
|
||||
captchaCode.value = Math.random().toString(36).substring(2, 7).toUpperCase();
|
||||
userInputCaptcha.value = "";
|
||||
};
|
||||
|
||||
const isCaptchaValid = computed(() => {
|
||||
return userInputCaptcha.value.toLowerCase() === captchaCode.value.toLowerCase();
|
||||
});
|
||||
|
||||
const isUpdateValid = computed(() => {
|
||||
return (
|
||||
canEditDueDate.value &&
|
||||
dateRecord.value.newDueDate &&
|
||||
!dateError.value &&
|
||||
isCaptchaValid.value &&
|
||||
userInputCaptcha.value.length > 0
|
||||
);
|
||||
});
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return "-";
|
||||
return dayjs(dateString).format("L");
|
||||
};
|
||||
|
||||
const fetchPaymentScheduleData = async () => {
|
||||
loadingData.value = true;
|
||||
try {
|
||||
const data = await $getdata("payment_schedule", { id: props.scheduleItemId }, undefined, true);
|
||||
paymentScheduleData.value = data;
|
||||
|
||||
// Set initial value for new due date
|
||||
if (data?.to_date) {
|
||||
dateRecord.value.newDueDate = data.to_date;
|
||||
}
|
||||
} catch (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",
|
||||
);
|
||||
} finally {
|
||||
loadingData.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdate = async () => {
|
||||
if (!isUpdateValid.value) {
|
||||
if (!isCaptchaValid.value) {
|
||||
$snackbar(
|
||||
isVietnamese.value ? "Vui lòng nhập đúng mã xác nhận." : "Please enter the correct confirmation code.",
|
||||
"Cảnh báo",
|
||||
"Warning",
|
||||
);
|
||||
} else if (dateError.value) {
|
||||
$snackbar(dateError.value, "Cảnh báo", "Warning");
|
||||
} else if (!canEditDueDate.value) {
|
||||
$snackbar(
|
||||
isVietnamese.value ? "Không thể thay đổi ngày đến hạn." : "Cannot change due date.",
|
||||
"Cảnh báo",
|
||||
"Warning",
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
|
||||
try {
|
||||
const response = await $patchapi("payment_schedule", {
|
||||
id: props.scheduleItemId,
|
||||
to_date: dateRecord.value.newDueDate,
|
||||
});
|
||||
|
||||
if (response !== "error") {
|
||||
$snackbar(
|
||||
isVietnamese.value ? "Cập nhật ngày đến hạn thành công!" : "Due date updated successfully!",
|
||||
"Thành công",
|
||||
"Success",
|
||||
);
|
||||
emit("updated");
|
||||
emit("close");
|
||||
} else {
|
||||
$snackbar(
|
||||
isVietnamese.value ? "Có lỗi xảy ra khi cập nhật ngày đến hạn." : "An error occurred while updating due date.",
|
||||
"Lỗi",
|
||||
"Error",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error updating due date:", e);
|
||||
$snackbar(
|
||||
isVietnamese.value ? "Có lỗi xảy ra khi cập nhật ngày đến hạn." : "An error occurred while updating due date.",
|
||||
"Lỗi",
|
||||
"Error",
|
||||
);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchPaymentScheduleData();
|
||||
generateCaptcha();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.notification {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
</style>
|
||||
@@ -1,608 +0,0 @@
|
||||
<template>
|
||||
<div class="px-3">
|
||||
<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..." }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="paymentScheduleData">
|
||||
<div class="content">
|
||||
<!-- Thông tin cơ bản (giữ nguyên) -->
|
||||
<div class="columns is-multiline is-mobile">
|
||||
<div class="column is-3">
|
||||
<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 || "-" }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<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>
|
||||
<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>
|
||||
</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>
|
||||
</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'"
|
||||
>
|
||||
{{ $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),
|
||||
}"
|
||||
>
|
||||
{{ formatDate(paymentScheduleData.to_date) }}
|
||||
<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>
|
||||
<p class="has-text-weight-bold has-text-danger">
|
||||
{{ 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>
|
||||
<p class="has-text-weight-bold has-text-success">
|
||||
{{ 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>
|
||||
<p class="has-text-weight-bold has-text-danger">
|
||||
{{ 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>
|
||||
<p class="has-text-weight-bold has-text-primary">
|
||||
{{ 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>
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<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 style="min-width: 3rem">
|
||||
<p class="is-size-5 has-text-weight-bold has-text-primary">
|
||||
{{ formatDate(item.entry.date) }}
|
||||
</p>
|
||||
<p
|
||||
v-if="item.entry.code"
|
||||
class="is-size-6 has-text-grey"
|
||||
>
|
||||
{{ item.entry.code }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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"
|
||||
>
|
||||
{{ item.entry.code }}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<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"
|
||||
>
|
||||
{{ $numtoString(item.entry.principal) }}
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
class="has-text-grey-light"
|
||||
>-</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<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"
|
||||
>
|
||||
{{ $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"
|
||||
>
|
||||
{{ $numtoString(item.principalRemain) }}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chi tiết lãi phát sinh -->
|
||||
<div class="">
|
||||
<!-- Diễn giải chi tiết cách tính lãi kỳ này -->
|
||||
<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'">
|
||||
{{ $numtoString(item.penaltyAccumulated) }}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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"
|
||||
>
|
||||
<p class="is-size-5">
|
||||
<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) }}
|
||||
</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) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<!-- Tóm tắt cuối cùng -->
|
||||
<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>
|
||||
</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'"
|
||||
>
|
||||
{{ $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>
|
||||
</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'"
|
||||
>
|
||||
{{ $numtoString(paymentScheduleData.penalty_remain) }}
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="isConfirmAllowed && $getEditRights()"
|
||||
class="field is-grouped is-grouped-left"
|
||||
>
|
||||
<p class="control">
|
||||
<button
|
||||
class="button is-info mx-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"
|
||||
>
|
||||
<span>Tính lãi</span>
|
||||
</button>
|
||||
</p>
|
||||
<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";
|
||||
|
||||
const props = defineProps({
|
||||
scheduleItemId: {
|
||||
type: [Number, String],
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["close", "confirmed"]);
|
||||
|
||||
const store = useStore();
|
||||
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 isUpdatingInvoice = ref(false);
|
||||
const showModal = ref(null);
|
||||
const showModalViewEmail = ref(null);
|
||||
|
||||
const isVietnamese = computed(() => store.lang === "vi");
|
||||
|
||||
const isOverdue = (dueDate) => {
|
||||
if (!dueDate) return false;
|
||||
return dayjs(dueDate).isBefore(dayjs(), "day");
|
||||
};
|
||||
|
||||
const isConfirmAllowed = computed(() => paymentScheduleData.value?.status === 1);
|
||||
const isInvoiceAllowed = computed(() => paymentScheduleData.value?.status === 2);
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return "-";
|
||||
return dayjs(dateString).format("L");
|
||||
};
|
||||
|
||||
const getEntryTypeLabel = (type) => {
|
||||
const labels = {
|
||||
PAYMENT: "Thanh toán",
|
||||
REDUCTION: "Miễn giảm",
|
||||
};
|
||||
return labels[type] || type;
|
||||
};
|
||||
|
||||
// Xử lý timeline
|
||||
const processedEntries = computed(() => {
|
||||
if (!paymentScheduleData.value?.entry) return [];
|
||||
|
||||
const relevantEntries = paymentScheduleData.value.entry.filter(
|
||||
(e) => e.type === "PAYMENT" && e.penalty_added_to_entry !== undefined,
|
||||
);
|
||||
|
||||
relevantEntries.sort((a, b) => new Date(a.date) - new Date(b.date));
|
||||
|
||||
let currentPrincipal = Number(paymentScheduleData.value.amount || 0);
|
||||
let totalPenaltyAccumulated = 0;
|
||||
let totalPenaltyPaid = Number(paymentScheduleData.value.penalty_paid || 0);
|
||||
let lastDate = paymentScheduleData.value.to_date;
|
||||
let principalBeforeThisPeriod = currentPrincipal;
|
||||
|
||||
const result = [];
|
||||
|
||||
relevantEntries.forEach((entry, index) => {
|
||||
const entryDate = dayjs(entry.date);
|
||||
const lastEventDate = dayjs(lastDate);
|
||||
const days = entryDate.diff(lastEventDate, "day");
|
||||
|
||||
let penaltyThisPeriod = Number(entry.penalty_added_to_entry || 0);
|
||||
|
||||
totalPenaltyAccumulated += penaltyThisPeriod;
|
||||
|
||||
if (entry.type === "PAYMENT") {
|
||||
const principalPaid = Number(entry.principal || 0);
|
||||
const penaltyPaid = Number(entry.penalty || 0);
|
||||
|
||||
currentPrincipal -= principalPaid;
|
||||
if (currentPrincipal < 0) currentPrincipal = 0;
|
||||
|
||||
totalPenaltyPaid += penaltyPaid;
|
||||
principalBeforeThisPeriod = currentPrincipal;
|
||||
}
|
||||
|
||||
lastDate = entry.date;
|
||||
|
||||
const penaltyRemain = totalPenaltyAccumulated - totalPenaltyPaid;
|
||||
|
||||
result.push({
|
||||
entry,
|
||||
principalRemain: Number(entry.amount_remain_after_allocation || 0), // Gốc còn lại sau bút toán này
|
||||
principalBefore: Number(entry.amount_remain_after_allocation || 0) + Number(entry.principal || 0), // Dư nợ trước khi trừ principal của bút toán này
|
||||
penaltyAccumulated: totalPenaltyAccumulated,
|
||||
penaltyThisPeriod,
|
||||
penaltyRemain,
|
||||
rate: Number(entry.DAILY_PENALTY_RATE || 0) * 100,
|
||||
totalDebt: Number(entry.amount_remain_after_allocation || 0) + penaltyThisPeriod,
|
||||
penaltyDetail: {
|
||||
from: lastEventDate.format("L"),
|
||||
to: entryDate.format("L"),
|
||||
days,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
// Tiền lãi hiện tại (từ entry gần nhất)
|
||||
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,
|
||||
);
|
||||
if (paymentEntries.length === 0) return null;
|
||||
return paymentEntries[paymentEntries.length - 1];
|
||||
});
|
||||
|
||||
const hasUnpaidDebt = computed(() => {
|
||||
return paymentScheduleData.value?.amount_remain > 0 || paymentScheduleData.value?.penalty_remain > 0;
|
||||
});
|
||||
|
||||
const latestPenaltyToThisEntry = computed(() => {
|
||||
return latestEntry.value?.penalty_to_this_entry || 0;
|
||||
});
|
||||
|
||||
const latestAdditionalPenalty = computed(() => {
|
||||
return paymentScheduleData.value?.penalty_amount - latestEntry.value?.penalty_to_this_entry || 0;
|
||||
});
|
||||
|
||||
const latestTotalPenalty = computed(() => {
|
||||
return latestPenaltyToThisEntry.value + latestAdditionalPenalty.value;
|
||||
});
|
||||
|
||||
const latestEntryCode = computed(() => {
|
||||
return latestEntry.value?.code || "-";
|
||||
});
|
||||
|
||||
const latestEntryDate = computed(() => {
|
||||
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);
|
||||
|
||||
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";
|
||||
|
||||
paymentScheduleData.value = data;
|
||||
|
||||
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);
|
||||
$snackbar(
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
const getPenalty = async () => {
|
||||
isLoading.value = true;
|
||||
const target_item = paymentScheduleData.value;
|
||||
const workflowPayload = {
|
||||
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");
|
||||
await fetchPaymentScheduleData();
|
||||
} catch (e) {
|
||||
$snackbar("Có lỗi xảy ra khi tính lãi.", "Lỗi", "Error");
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
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",
|
||||
);
|
||||
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,
|
||||
);
|
||||
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",
|
||||
);
|
||||
await fetchPaymentScheduleData();
|
||||
} catch (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");
|
||||
} finally {
|
||||
isUpdatingInvoice.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const resetInvoiceData = () => {
|
||||
invoiceData.value = { link: "", ref_code: "" };
|
||||
};
|
||||
|
||||
function openEntryDetailModal(entry) {
|
||||
if (!entry.code) return;
|
||||
showModal.value = {
|
||||
component: "accounting/InternalEntry",
|
||||
title: `Chi tiết bút toán: ${entry.code}`,
|
||||
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,
|
||||
);
|
||||
showModalViewEmail.value = {
|
||||
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,
|
||||
},
|
||||
onConfirm: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
const handleModalClose = () => {
|
||||
showModal.value = null;
|
||||
};
|
||||
|
||||
const handleConfirmDelete = () => {
|
||||
showModal.value = null;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchPaymentScheduleData();
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user