801 lines
26 KiB
Vue
801 lines
26 KiB
Vue
<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 -->
|
||
<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"
|
||
>
|
||
<!-- ===================== REDUCTION ===================== -->
|
||
<template v-if="item.isReduction">
|
||
<div style="min-width: 3rem">
|
||
<p class="is-size-5 has-text-weight-bold has-text-info">
|
||
{{ 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-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"
|
||
>
|
||
{{ item.entry.code }}
|
||
</span>
|
||
</p>
|
||
|
||
<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"
|
||
>
|
||
{{ $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'
|
||
"
|
||
>
|
||
{{ $numtoString(item.penaltyRemain) }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- ===================== PAYMENT ===================== -->
|
||
<template v-else>
|
||
<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="">
|
||
<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>
|
||
</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"
|
||
>
|
||
<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
|
||
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>
|
||
</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 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"
|
||
>
|
||
<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('DD/MM/YYYY');
|
||
};
|
||
|
||
const getEntryTypeLabel = (type) => {
|
||
const labels = {
|
||
PAYMENT: 'Thanh toán',
|
||
REDUCTION: 'Miễn giảm',
|
||
};
|
||
return labels[type] || type;
|
||
};
|
||
|
||
// ============================================================
|
||
// Xử lý timeline — gộp PAYMENT + REDUCTION, sort theo date
|
||
// ============================================================
|
||
const processedEntries = computed(() => {
|
||
if (!paymentScheduleData.value?.entry) return [];
|
||
|
||
const relevantEntries = paymentScheduleData.value.entry.filter(
|
||
(e) =>
|
||
(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 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;
|
||
|
||
const result = [];
|
||
|
||
relevantEntries.forEach((entry) => {
|
||
const entryDate = dayjs(entry.date);
|
||
|
||
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;
|
||
if (currentPrincipal < 0) currentPrincipal = 0;
|
||
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
|
||
// Nếu lần trước trả trước hạn: max trả về to_date → from = to_date
|
||
// Nếu lần trước trả sau hạn: max trả về lastDate → from = lastDate
|
||
const lastEventDate = lastDate
|
||
? dayjs(Math.max(dayjs(toDate).valueOf(), dayjs(lastDate).valueOf()))
|
||
: dayjs(toDate);
|
||
const days = Math.max(0, entryDate.diff(lastEventDate, 'day'));
|
||
|
||
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,
|
||
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,
|
||
penaltyDetail: {
|
||
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;
|
||
// REDUCTION không dịch chuyển lastDate
|
||
|
||
result.push({
|
||
entry,
|
||
isReduction: true,
|
||
reduceAmount,
|
||
penaltyRemain: Math.max(
|
||
0,
|
||
totalPenaltyAccumulated - totalPenaltyPaid - totalPenaltyReduce,
|
||
),
|
||
});
|
||
}
|
||
});
|
||
|
||
return result;
|
||
});
|
||
|
||
// Tiền lãi hiện tại (từ entry PAYMENT 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(
|
||
() => latestEntry.value?.penalty_to_this_entry || 0,
|
||
);
|
||
|
||
const latestAdditionalPenalty = computed(() => {
|
||
return (
|
||
paymentScheduleData.value?.penalty_amount -
|
||
(latestEntry.value?.penalty_to_this_entry || 0) || 0
|
||
);
|
||
});
|
||
|
||
const latestTotalPenalty = computed(
|
||
() => latestPenaltyToThisEntry.value + latestAdditionalPenalty.value,
|
||
);
|
||
|
||
const latestEntryCode = computed(() => 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(
|
||
'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';
|
||
|
||
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>
|