Files
web/app/components/application/PaymentSchedulePresentation.vue
2026-03-02 09:45:33 +07:00

504 lines
23 KiB
Vue

<template>
<div v-if="productData" class="grid px-3">
<div class="cell is-col-span-12">
<div id="schedule-content">
<div v-if="selectedPolicy" id="print-area" :class="{ 'is-loading': isLoading }">
<!-- Header -->
<div class="is-flex is-justify-content-space-between is-align-items-center">
<h3 class="title is-4 has-text-primary mb-1">
{{ selectedPolicy.name }}
</h3>
<div>
<span class="button is-white">
<span class="has-text-weight-semibold">Đơn vị: VNĐ</span>
</span>
<button class="button is-light" @click="$emit('print')" id="ignore-print">
<span class="is-size-6">In</span>
</button>
</div>
</div>
<hr class="my-4" style="background-color: var(--bulma-background)"></hr>
<!-- Summary Information -->
<div class="fixed-grid has-4-cols-mobile has-7-cols-desktop">
<div class="grid">
<div class="cell is-col-span-6-mobile is-col-span-1-desktop">
<p class="is-size-6 has-text-weight-bold mb-1">Sản phẩm</p>
<p class="has-text-primary has-text-weight-medium">{{ productData.trade_code || productData.code }} <a
class="ml-4" id="ignore" @click="$copyToClipboard(productData.trade_code)">
<SvgIcon name="copy.svg" type="primary" :size="18" />
</a>
</p>
</div>
<div class="cell is-col-span-6-mobile is-col-span-1-desktop">
<p class="is-size-6 has-text-weight-bold mb-1">Giá niêm yết</p>
<p class="has-text-primary">{{ $numtoString(calculatorData.originPrice) }}</p>
</div>
<div class="cell is-col-span-6-mobile is-col-span-1-desktop">
<p class="is-size-6 has-text-weight-bold mb-1">Tổng chiết khấu</p>
<p class="has-text-danger has-text-weight-bold">{{ $numtoString(calculatorData.totalDiscount) }}</p>
</div>
<div class="cell is-col-span-6-mobile is-col-span-1-desktop">
<p class="is-size-6 has-text-weight-bold mb-1">Giá sau chiết khấu</p>
<p class="has-text-black has-text-weight-bold">{{ $numtoString(calculatorData.salePrice) }}</p>
</div>
<div v-if="selectedPolicy.contract_allocation_percentage < 100"
class="cell is-col-span-6-mobile is-col-span-1-desktop">
<p class="is-size-6 has-text-weight-bold mb-1">Giá trị bảo đảm</p>
<p class="has-text-primary">{{ $numtoString(calculatorData.allocatedPrice) }}</p>
</div>
<div v-if="totalPaid === 0" class="cell is-col-span-6-mobile is-col-span-1-desktop">
<p class="is-size-6 has-text-weight-bold mb-1">Đặt cọc</p>
<p class="has-text-primary">{{ $numtoString(selectedPolicy.deposit) }}</p>
</div>
<div class="cell is-col-span-6-mobile is-col-span-1-desktop">
<p class="is-size-6 has-text-weight-bold mb-1">Khách hàng</p>
<p v-if="selectedCustomer" class="has-text-primary has-text-weight-medium">
{{ selectedCustomer.code }} - {{ selectedCustomer.fullname }}
</p>
<p v-else class="has-text-grey is-italic is-size-6">Chưa chọn</p>
</div>
</div>
</div>
<hr class="my-4" style="background-color: var(--bulma-background)"></hr>
<!-- Detailed Discounts -->
<div v-if="calculatorData.detailedDiscounts && calculatorData.detailedDiscounts.length > 0" class="mt-4 mb-4">
<p class="has-text-weight-bold is-size-5 mb-2 has-text-primary is-underlined">
CHI TIẾT CHIẾT KHẤU:
</p>
<table class="table is-fullwidth is-hoverable is-size-6">
<thead>
<tr>
<th class="has-background-primary has-text-white has-font-weight-normal" style="border: none;"
colspan="2">Diễn giải chiết khấu</th>
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right"
style="border: none;" width="15%">Giá trị</th>
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right"
style="border: none;" width="20%">Thành tiền</th>
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right"
style="border: none;" width="20%">Còn lại</th>
</tr>
</thead>
<tbody>
<tr style="border-bottom: 1px solid #f5f5f5;" class="has-text-grey-light">
<td colspan="4" class="has-text-right pt-1 pb-1">Giá gốc</td>
<td class="has-text-right has-text-weight-bold pt-1 pb-1">{{
$numtoString(calculatorData.originPrice) }}</td>
</tr>
<tr v-for="(item, idx) in calculatorData.detailedDiscounts" :key="`discount-${idx}`"
style="border-bottom: 1px solid #f5f5f5;">
<td width="5%" class="has-text-centered">{{ idx + 1 }}</td>
<td>
<span class="has-text-weight-semibold">{{ item.name }}</span>
<span class="tag is-primary has-text-white is-rounded border ml-1">{{ item.code }}</span>
</td>
<td class="has-text-right">{{ item.customType === 1 ? item.customValue + '%' :
$numtoString(item.customValue) }}</td>
<td class="has-text-right has-text-danger">-{{ $numtoString(item.amount) }}</td>
<td class="has-text-right has-text-primary">{{ $numtoString(item.remaining) }}</td>
</tr>
</tbody>
</table>
</div>
<!-- Early Payment Details -->
<div v-if="isEarlyPaymentActive" class="mt-4 mb-4">
<!-- Original Schedule -->
<p class="has-text-weight-bold is-size-5 mb-2 has-text-primary is-underlined">
LỊCH THANH TOÁN GỐC (THEO CHÍNH SÁCH)
</p>
<div class="table-container schedule-container mb-4">
<table class="table is-fullwidth is-hoverable is-size-6">
<thead>
<tr>
<th class="has-background-primary has-text-white has-font-weight-normal" style="border: none;">Đợt
</th>
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right"
style="border: none;">Tỷ lệ</th>
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right"
style="border: none;">Số tiền (VND)</th>
<th class="has-background-primary has-text-white has-font-weight-normal" style="border: none;">Ngày
bắt đầu</th>
<th class="has-background-primary has-text-white has-font-weight-normal" style="border: none;">Ngày
đến hạn</th>
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right"
style="border: none;">Số ngày</th>
</tr>
</thead>
<tbody>
<tr v-for="(plan, index) in calculatorData.originalPaymentSchedule" :key="`orig-plan-${index}`"
style="border-bottom: 1px solid #f5f5f5;">
<td class="has-text-weight-semibold">Đợt {{ plan.cycle }}</td>
<td class="has-text-right">{{ plan.type === 1 ? `${plan.value}%` : '-' }}</td>
<td class="has-text-right">{{ $numtoString(plan.amount) }}</td>
<td>{{ formatDate(plan.from_date) }}</td>
<td>{{ formatDate(plan.to_date) }}</td>
<td class="has-text-right">{{ plan.days }}</td>
</tr>
</tbody>
</table>
</div>
<!-- Early Discount Calculation Details -->
<p class="has-text-weight-bold is-size-5 mb-2 has-text-primary is-underlined">
DIỄN GIẢI CHIẾT KHẤU THANH TOÁN SỚM
</p>
<div class="table-container schedule-container">
<table class="table is-fullwidth is-hoverable is-size-6">
<thead>
<tr>
<th class="has-background-primary has-text-white has-font-weight-normal" style="border: none;">Đợt
</th>
<th class="has-background-primary has-text-white has-font-weight-normal" style="border: none;">Hạn
TT Gốc</th>
<th class="has-background-primary has-text-white has-font-weight-normal" style="border: none;">Ngày
TT Thực Tế</th>
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right"
style="border: none;">Số tiền gốc</th>
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right"
style="border: none;">Số ngày TT sớm</th>
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right"
style="border: none;">Tỷ lệ CK (%/ngày)</th>
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right"
style="border: none;">Tiền chiết khấu</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, idx) in calculatorData.earlyDiscountDetails" :key="`early-discount-${idx}`"
style="border-bottom: 1px solid #f5f5f5;">
<td>Đợt {{ item.cycle }}</td>
<td>{{ formatDate(item.original_payment_date) }}</td>
<td>{{ formatDate(item.actual_payment_date) }}</td>
<td class="has-text-right">{{ $numtoString(item.original_amount) }}</td>
<td class="has-text-right">{{ item.early_days }}</td>
<td class="has-text-right">{{ item.discount_rate }}</td>
<td class="has-text-right has-text-danger">-{{ $numtoString(item.discount_amount) }}</td>
</tr>
</tbody>
<tfoot>
<tr class="has-background-light">
<th colspan="6" class="has-text-right has-text-weight-bold">Tổng chiết khấu thanh toán sớm</th>
<th class="has-text-right has-text-weight-bold has-text-danger">-{{
$numtoString(totalEarlyDiscount) }}</th>
</tr>
</tfoot>
</table>
</div>
</div>
<!-- Payment Schedule Table -->
<div v-if="displaySchedule.length > 0" class="mt-4">
<div class="level m-0 mb-2 is-mobile">
<div class="level-left">
<p class="has-text-weight-bold is-size-5 has-text-primary is-underlined">
<span v-if="isEarlyPaymentActive">LỊCH THANH TOÁN CUỐI CÙNG</span>
<span v-else>LỊCH THANH TOÁN</span>
</p>
</div>
<div class="level-right" id="ignore-print">
<div class="buttons are-small has-addons">
<button class="button" @click="viewMode = 'table'"
:class="viewMode === 'table' ? 'is-link is-selected' : 'is-light'">
<span class="is-size-6">Bảng</span>
</button>
<button class="button" @click="viewMode = 'list'"
:class="viewMode === 'list' ? 'is-link is-selected' : 'is-light'">
<span class="is-size-6">Thẻ</span>
</button>
</div>
</div>
</div>
<!-- Table View -->
<div v-if="viewMode === 'table'" class="table-container schedule-container">
<table class="table is-fullwidth is-hoverable is-size-6">
<thead>
<tr>
<th class="has-background-primary has-text-white has-font-weight-normal" style="border: none;">Đợt
thanh toán</th>
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right"
style="border: none;">Số tiền (VND)</th>
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right"
style="border: none;">Đã thanh toán</th>
<th class="has-background-primary has-text-white has-font-weight-normal has-text-right"
style="border: none;">Còn phải TT</th>
<th class="has-background-primary has-text-white has-font-weight-normal" style="border: none;">Ngày
bắt đầu</th>
<th class="has-background-primary has-text-white has-font-weight-normal" style="border: none;">Ngày
đến hạn</th>
<th class="has-background-primary has-text-white has-font-weight-normal" style="border: none;">Trạng
thái</th>
</tr>
</thead>
<tbody>
<tr v-for="(plan, index) in displaySchedule" :key="`plan-${index}`"
style="border-bottom: 1px solid #f5f5f5;"
:class="plan.is_merged ? 'has-background-warning-light' : ''">
<td class="has-text-weight-semibold" :class="plan.is_merged ? 'has-text-warning' : ''">
Đợt {{ plan.cycle }}
<span v-if="plan.is_merged" class="tag is-warning is-light ml-1 is-size-7">GỘP SỚM</span>
</td>
<td class="has-text-right">
<div v-if="plan.is_merged" class="has-text-right">
<p class="has-text-grey" title="Tổng các đợt gốc">{{ $numtoString(totalOriginalEarlyAmount) }}
</p>
<p class="has-text-danger" title="Chiết khấu thanh toán sớm">- {{
$numtoString(totalEarlyDiscount) }}</p>
<hr class="my-1"
style="background: hsla(0, 0%, 0%, 0.2); margin-left: auto; width: 50%;">
<p class="has-text-weight-bold">{{ $numtoString(plan.amount) }}</p>
</div>
<span v-else>{{ $numtoString(plan.amount) }}</span>
</td>
<td class="has-text-right has-text-success">{{ $numtoString(plan.paid_amount) }}</td>
<td class="has-text-right has-text-danger">{{ $numtoString(plan.remain_amount) }}</td>
<td>{{ formatDate(plan.from_date) }}</td>
<td>{{ formatDate(plan.to_date) }}</td>
<td>
<span v-if="plan.status === 2" class="tag is-success">Đã thanh toán</span>
<span v-else class="tag is-warning">Chờ thanh toán</span>
</td>
</tr>
</tbody>
<tfoot>
<tr class="has-background-light">
<th class="has-text-right has-text-weight-bold">Tổng cộng</th>
<th class="has-text-right has-text-weight-bold">{{ $numtoString(totalAmount) }}</th>
<th class="has-text-right has-text-weight-bold has-text-success">{{ $numtoString(totalPaid) }}
</th>
<th class="has-text-right has-text-weight-bold has-text-danger">{{
$numtoString(calculatorData.totalRemaining) }}</th>
<th colspan="3"></th>
</tr>
</tfoot>
</table>
</div>
<!-- List View (Card) -->
<div v-else-if="viewMode === 'list'" class="schedule-container">
<div v-for="(plan, index) in displaySchedule" :key="`card-${index}`" class="card mb-4"
:class="plan.is_merged ? 'has-background-warning-light' : ''">
<div class="card-content">
<div class="level is-mobile mb-5">
<div class="level-left">
<div class="level-item">
<span class="tag is-primary" :class="plan.is_merged ? 'is-warning' : ''">Đợt {{ plan.cycle
}}</span>
<span v-if="plan.is_merged" class="tag is-warning is-light ml-1 is-size-7">GỘP SỚM</span>
</div>
</div>
<div class="level-right">
<div class="level-item has-text-weight-bold">
<div v-if="plan.is_merged" class="has-text-right">
<p class="has-text-grey" title="Tổng các đợt gốc">{{ $numtoString(totalOriginalEarlyAmount)
}}</p>
<p class="has-text-danger" title="Chiết khấu thanh toán sớm">- {{
$numtoString(totalEarlyDiscount) }}</p>
<hr class="my-1"
style="background: hsla(0, 0%, 0%, 0.2); margin-left: auto">
<p class="has-text-weight-bold">{{ $numtoString(plan.amount) }}</p>
</div>
<span v-else>{{ $numtoString(plan.amount) }}</span>
</div>
</div>
</div>
<div class="level is-mobile mb-1">
<div class="level-left">Đã thanh toán:</div>
<div class="level-right has-text-success">{{ $numtoString(plan.paid_amount) }}</div>
</div>
<div class="level is-mobile mb-1">
<div class="level-left">Còn phải TT:</div>
<div class="level-right has-text-danger">{{ $numtoString(plan.remain_amount) }}</div>
</div>
<div class="level is-mobile mb-1">
<div class="level-left">Từ ngày:</div>
<div class="level-right">{{ formatDate(plan.from_date) }}</div>
</div>
<div class="level is-mobile mb-1">
<div class="level-left">Đến hạn:</div>
<div class="level-right">{{ formatDate(plan.to_date) }}</div>
</div>
<div class="level is-mobile">
<div class="level-left">Trạng thái:</div>
<div class="level-right">
<span v-if="plan.status === 2" class="tag is-success">Đã thanh toán</span>
<span v-else class="tag is-warning">Chờ thanh toán</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Summary Footer -->
<div class="" style="border-top: 1px solid #eee;">
<div class="level is-mobile is-size-6 my-4">
<div class="level-right">
<div class="level-item">
<span class="is-uppercase is-size-4 has-text-weight-semibold">Tổng cộng:&nbsp;</span>
<span class="has-text-success has-text-weight-bold is-size-4">
{{ $numtoString(calculatorData.allocatedPrice) }}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
import dayjs from 'dayjs';
// Props - CHỈ NHẬN DỮ LIỆU ĐÃ TÍNH TOÁN
const props = defineProps({
productData: {
type: Object,
default: null
},
selectedPolicy: {
type: Object,
default: null
},
selectedCustomer: {
type: Object,
default: null
},
calculatorData: {
type: Object,
required: true,
// Cấu trúc:
// {
// originPrice: number,
// totalDiscount: number,
// salePrice: number,
// allocatedPrice: number,
// originalPaymentSchedule: array,
// finalPaymentSchedule: array,
// earlyDiscountDetails: array,
// totalRemaining: number,
// detailedDiscounts: array,
// baseDate: Date
// }
},
isLoading: {
type: Boolean,
default: false
}
});
// Emits
const emit = defineEmits(['print']);
// Local state
const viewMode = ref('table');
// Computed - CHỈ HIỂN THỊ, KHÔNG TÍNH TOÁN
const displaySchedule = computed(() => {
return props.calculatorData?.finalPaymentSchedule || [];
});
const isEarlyPaymentActive = computed(() => {
return props.calculatorData.earlyDiscountDetails && props.calculatorData.earlyDiscountDetails.length > 0;
});
const totalEarlyDiscount = computed(() => {
return props.calculatorData.earlyDiscountDetails?.reduce((sum, item) => sum + item.discount_amount, 0) || 0;
});
const totalOriginalEarlyAmount = computed(() => {
return props.calculatorData.earlyDiscountDetails?.reduce((sum, item) => sum + item.original_amount, 0) || 0;
});
const totalAmount = computed(() => {
return displaySchedule.value.reduce((sum, plan) => sum + plan.amount, 0);
});
const totalPaid = computed(() => {
return displaySchedule.value.reduce((sum, plan) => sum + plan.paid_amount, 0);
});
const formatDate = (date) => {
if (!date) return '-';
return dayjs(date).format('DD/MM/YYYY');
};
</script>
<style scoped>
.table-container.schedule-container thead th {
position: sticky;
top: 0;
background: white;
z-index: 2;
border-bottom: 1px solid #dbdbdb !important;
}
.table-container {
max-height: 400px;
overflow-y: auto;
}
.border {
border: 1px solid #dbdbdb;
}
li.is-active a,
li a:hover {
color: white !important;
background-color: #204853 !important;
transition: all 0.3s ease;
}
.content {
display: block;
}
.content p {
margin: 0;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
tr,
td,
th,
.card {
page-break-inside: avoid !important;
}
@media print {
.schedule-container {
max-height: none;
overflow: visible;
}
.table-container.schedule-container thead th {
position: static;
}
#ignore-print {
display: none !important;
}
tr,
td,
th {
page-break-inside: avoid !important;
}
}
</style>