chore: install prettier
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
pagename: 'debt_report',
|
||||
api: 'transaction',
|
||||
timeopt: { time: 36000, disable: ['add'] },
|
||||
filter: { phase: 3 }
|
||||
filter: { phase: 3 },
|
||||
}"
|
||||
@option="handleTimeOption"
|
||||
@excel="exportExcel"
|
||||
@@ -15,17 +15,26 @@
|
||||
/>
|
||||
|
||||
<!-- Loading -->
|
||||
<div v-if="loading" class="has-text-centered py-6">
|
||||
<div
|
||||
v-if="loading"
|
||||
class="has-text-centered py-6"
|
||||
>
|
||||
<p class="has-text-grey mb-3">Đang tải dữ liệu...</p>
|
||||
<progress class="progress is-small is-primary" max="100"></progress>
|
||||
<progress
|
||||
class="progress is-small is-primary"
|
||||
max="100"
|
||||
></progress>
|
||||
</div>
|
||||
|
||||
<div class="" v-else>
|
||||
<div
|
||||
class=""
|
||||
v-else
|
||||
>
|
||||
<!-- Table -->
|
||||
<div
|
||||
v-if="filteredRows.length > 0"
|
||||
class="table-container"
|
||||
style="overflow-x: auto; max-width: 100%;"
|
||||
style="overflow-x: auto; max-width: 100%"
|
||||
>
|
||||
<table class="table is-bordered is-striped is-narrow is-hoverable is-fullwidth debt-table">
|
||||
<thead>
|
||||
@@ -37,22 +46,40 @@
|
||||
>
|
||||
STT
|
||||
</th>
|
||||
<th rowspan="2" class="fixed-col has-background-primary has-text-white">
|
||||
<th
|
||||
rowspan="2"
|
||||
class="fixed-col has-background-primary has-text-white"
|
||||
>
|
||||
Mã KH
|
||||
</th>
|
||||
<th rowspan="2" class="fixed-col has-background-primary has-text-white">
|
||||
<th
|
||||
rowspan="2"
|
||||
class="fixed-col has-background-primary has-text-white"
|
||||
>
|
||||
Mã Căn
|
||||
</th>
|
||||
<th rowspan="2" class="fixed-col has-background-primary has-text-white">
|
||||
<th
|
||||
rowspan="2"
|
||||
class="fixed-col has-background-primary has-text-white"
|
||||
>
|
||||
Ngày ký HĐ
|
||||
</th>
|
||||
<th rowspan="2" class="fixed-col has-background-primary has-text-white">
|
||||
<th
|
||||
rowspan="2"
|
||||
class="fixed-col has-background-primary has-text-white"
|
||||
>
|
||||
Giá trị HĐMB
|
||||
</th>
|
||||
<th rowspan="2" class="fixed-col has-background-primary has-text-white">
|
||||
<th
|
||||
rowspan="2"
|
||||
class="fixed-col has-background-primary has-text-white"
|
||||
>
|
||||
Tiền nộp theo HĐV/TTTHNV
|
||||
</th>
|
||||
<th rowspan="2" class="fixed-col has-background-primary has-text-white">
|
||||
<th
|
||||
rowspan="2"
|
||||
class="fixed-col has-background-primary has-text-white"
|
||||
>
|
||||
Tỷ lệ
|
||||
</th>
|
||||
|
||||
@@ -66,13 +93,19 @@
|
||||
{{ sch.label }}
|
||||
</th>
|
||||
|
||||
<th rowspan="2" class="has-background-primary has-text-white">
|
||||
<th
|
||||
rowspan="2"
|
||||
class="has-background-primary has-text-white"
|
||||
>
|
||||
Số tiền quá hạn
|
||||
</th>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<template v-for="(sch, si) in scheduleHeaders" :key="si">
|
||||
<template
|
||||
v-for="(sch, si) in scheduleHeaders"
|
||||
:key="si"
|
||||
>
|
||||
<th class="has-background-primary has-text-white sub-header">Ngày</th>
|
||||
<th class="has-background-primary has-text-white sub-header">Số tiền</th>
|
||||
<th class="has-background-primary has-text-white sub-header">Lũy kế sang đợt</th>
|
||||
@@ -84,13 +117,20 @@
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr v-for="(row, ri) in filteredRows" :key="ri">
|
||||
<tr
|
||||
v-for="(row, ri) in filteredRows"
|
||||
:key="ri"
|
||||
>
|
||||
<!-- Fixed columns -->
|
||||
<td class="fixed-col has-text-centered">{{ ri + 1 }}</td>
|
||||
<td class="fixed-col">{{ row.customer_code }}</td>
|
||||
<td class="fixed-col has-text-weight-semibold has-text-primary">{{ row.trade_code }}</td>
|
||||
<td class="fixed-col has-text-weight-semibold has-text-primary">
|
||||
{{ row.trade_code }}
|
||||
</td>
|
||||
<td class="fixed-col">{{ row.contract_date }}</td>
|
||||
<td class="fixed-col has-text-right">{{ fmt(row.sale_price) }}</td>
|
||||
<td class="fixed-col has-text-right">
|
||||
{{ fmt(row.sale_price) }}
|
||||
</td>
|
||||
<td class="fixed-col has-text-right has-text-weight-semibold has-background-warning-light">
|
||||
{{ fmt(row.ttthnv_paid) }}
|
||||
</td>
|
||||
@@ -99,11 +139,18 @@
|
||||
</td>
|
||||
|
||||
<!-- Scrollable columns -->
|
||||
<template v-for="(sch, si) in scheduleHeaders" :key="si">
|
||||
<template
|
||||
v-for="(sch, si) in scheduleHeaders"
|
||||
:key="si"
|
||||
>
|
||||
<template v-if="row.schedules[si]">
|
||||
<td>{{ row.schedules[si].to_date }}</td>
|
||||
<td class="has-text-right">{{ fmt(row.schedules[si].amount) }}</td>
|
||||
<td class="has-text-right has-text-info">{{ fmt(row.schedules[si].luy_ke_sang_dot) }}</td>
|
||||
<td class="has-text-right">
|
||||
{{ fmt(row.schedules[si].amount) }}
|
||||
</td>
|
||||
<td class="has-text-right has-text-info">
|
||||
{{ fmt(row.schedules[si].luy_ke_sang_dot) }}
|
||||
</td>
|
||||
<td class="has-text-right has-text-weight-semibold has-text-success">
|
||||
{{ fmt(row.schedules[si].thuc_thanh_toan) }}
|
||||
</td>
|
||||
@@ -121,7 +168,13 @@
|
||||
</td>
|
||||
</template>
|
||||
<template v-else>
|
||||
<td colspan="6" class="has-text-centered has-text-grey-light" style="font-style: italic">—</td>
|
||||
<td
|
||||
colspan="6"
|
||||
class="has-text-centered has-text-grey-light"
|
||||
style="font-style: italic"
|
||||
>
|
||||
—
|
||||
</td>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -129,25 +182,35 @@
|
||||
class="has-text-right has-text-weight-bold"
|
||||
:class="Number(row.overdue) > 0 ? 'has-background-danger-light' : ''"
|
||||
>
|
||||
{{ Number(row.overdue) > 0 ? fmt(row.overdue) : '—' }}
|
||||
{{ Number(row.overdue) > 0 ? fmt(row.overdue) : "—" }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
<tfoot>
|
||||
<tr class="has-background-light">
|
||||
<td :colspan="7" class="fixed-col has-text-right has-text-weight-bold">TỔNG CỘNG:</td>
|
||||
<td
|
||||
:colspan="7"
|
||||
class="fixed-col has-text-right has-text-weight-bold"
|
||||
>
|
||||
TỔNG CỘNG:
|
||||
</td>
|
||||
|
||||
<template v-for="(sch, si) in scheduleHeaders" :key="si">
|
||||
<template
|
||||
v-for="(sch, si) in scheduleHeaders"
|
||||
:key="si"
|
||||
>
|
||||
<td></td>
|
||||
<td class="has-text-right has-text-weight-semibold">{{ fmt(colSum(si, 'amount')) }}</td>
|
||||
<td class="has-text-right has-text-weight-semibold">
|
||||
{{ fmt(colSum(si, "amount")) }}
|
||||
</td>
|
||||
<td></td>
|
||||
<td class="has-text-right has-text-weight-semibold has-text-success">
|
||||
{{ fmt(colSum(si, 'thuc_thanh_toan')) }}
|
||||
{{ fmt(colSum(si, "thuc_thanh_toan")) }}
|
||||
</td>
|
||||
<td></td>
|
||||
<td class="has-text-right has-text-weight-semibold has-text-danger">
|
||||
{{ fmt(colSum(si, 'amount_remain')) }}
|
||||
{{ fmt(colSum(si, "amount_remain")) }}
|
||||
</td>
|
||||
</template>
|
||||
|
||||
@@ -160,7 +223,10 @@
|
||||
</div>
|
||||
|
||||
<!-- Empty -->
|
||||
<div v-else class="has-text-centered py-6">
|
||||
<div
|
||||
v-else
|
||||
class="has-text-centered py-6"
|
||||
>
|
||||
<p class="has-text-grey">Không có dữ liệu</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -168,283 +234,279 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import TimeOption from '~/components/datatable/TimeOption'
|
||||
const { $findapi, $getapi, $dayjs, $copy } = useNuxtApp()
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import TimeOption from "~/components/datatable/TimeOption";
|
||||
const { $findapi, $getapi, $dayjs, $copy } = useNuxtApp();
|
||||
|
||||
const loading = ref(false)
|
||||
const rows = ref([])
|
||||
const filteredRows = ref([])
|
||||
const scheduleHeaders = ref([])
|
||||
const loading = ref(false);
|
||||
const rows = ref([]);
|
||||
const filteredRows = ref([]);
|
||||
const scheduleHeaders = ref([]);
|
||||
|
||||
const currentFilter = ref(null)
|
||||
const currentSearch = ref(null)
|
||||
const currentFilter = ref(null);
|
||||
const currentSearch = ref(null);
|
||||
|
||||
function handleTimeOption(option) {
|
||||
if (!option) {
|
||||
currentFilter.value = null
|
||||
currentSearch.value = null
|
||||
applyFilters()
|
||||
return
|
||||
currentFilter.value = null;
|
||||
currentSearch.value = null;
|
||||
applyFilters();
|
||||
return;
|
||||
}
|
||||
|
||||
if (option.filter) {
|
||||
currentFilter.value = option.filter
|
||||
currentSearch.value = null
|
||||
applyFilters()
|
||||
currentFilter.value = option.filter;
|
||||
currentSearch.value = null;
|
||||
applyFilters();
|
||||
} else if (option.filter_or) {
|
||||
currentFilter.value = null
|
||||
currentSearch.value = option.filter_or
|
||||
applyFilters()
|
||||
currentFilter.value = null;
|
||||
currentSearch.value = option.filter_or;
|
||||
applyFilters();
|
||||
}
|
||||
}
|
||||
|
||||
function applyFilters() {
|
||||
let filtered = [...rows.value]
|
||||
let filtered = [...rows.value];
|
||||
|
||||
if (currentFilter.value && currentFilter.value.create_time__date__gte) {
|
||||
const filterDate = new Date(currentFilter.value.create_time__date__gte)
|
||||
filtered = filtered.filter(row => {
|
||||
const contractDate = row.contract_date_raw ? new Date(row.contract_date_raw) : null
|
||||
return contractDate && contractDate >= filterDate
|
||||
})
|
||||
const filterDate = new Date(currentFilter.value.create_time__date__gte);
|
||||
filtered = filtered.filter((row) => {
|
||||
const contractDate = row.contract_date_raw ? new Date(row.contract_date_raw) : null;
|
||||
return contractDate && contractDate >= filterDate;
|
||||
});
|
||||
}
|
||||
|
||||
if (currentSearch.value) {
|
||||
const searchTerms = Object.values(currentSearch.value).map(v =>
|
||||
String(v).toLowerCase().replace('__icontains', '')
|
||||
)
|
||||
filtered = filtered.filter(row => {
|
||||
const searchTerms = Object.values(currentSearch.value).map((v) =>
|
||||
String(v).toLowerCase().replace("__icontains", ""),
|
||||
);
|
||||
filtered = filtered.filter((row) => {
|
||||
const searchableText = [
|
||||
row.customer_code,
|
||||
row.customer_name,
|
||||
row.trade_code,
|
||||
row.contract_date,
|
||||
String(row.sale_price)
|
||||
].join(' ').toLowerCase()
|
||||
return searchTerms.some(term => searchableText.includes(term))
|
||||
})
|
||||
String(row.sale_price),
|
||||
]
|
||||
.join(" ")
|
||||
.toLowerCase();
|
||||
return searchTerms.some((term) => searchableText.includes(term));
|
||||
});
|
||||
}
|
||||
|
||||
filteredRows.value = filtered
|
||||
filteredRows.value = filtered;
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
rows.value = []
|
||||
filteredRows.value = []
|
||||
scheduleHeaders.value = []
|
||||
loading.value = true;
|
||||
rows.value = [];
|
||||
filteredRows.value = [];
|
||||
scheduleHeaders.value = [];
|
||||
|
||||
try {
|
||||
const txnConn = $copy($findapi('transaction'))
|
||||
const txnConn = $copy($findapi("transaction"));
|
||||
txnConn.params = {
|
||||
filter: { phase: 3 },
|
||||
values: 'id,code,date,customer,customer__code,customer__fullname,sale_price,amount_received,amount_remain,product,product__trade_code,phase',
|
||||
sort: 'id'
|
||||
}
|
||||
values:
|
||||
"id,code,date,customer,customer__code,customer__fullname,sale_price,amount_received,amount_remain,product,product__trade_code,phase",
|
||||
sort: "id",
|
||||
};
|
||||
|
||||
const detailConn = $copy($findapi('reservation'))
|
||||
const detailConn = $copy($findapi("reservation"));
|
||||
detailConn.params = {
|
||||
filter: { transaction__phase: 3, phase: 3 },
|
||||
values: 'id,transaction,phase,amount,amount_received,amount_remaining,status',
|
||||
sort: 'transaction'
|
||||
}
|
||||
values: "id,transaction,phase,amount,amount_received,amount_remaining,status",
|
||||
sort: "transaction",
|
||||
};
|
||||
|
||||
const schConn = $copy($findapi('payment_schedule'))
|
||||
const schConn = $copy($findapi("payment_schedule"));
|
||||
schConn.params = {
|
||||
filter: { txn_detail__phase: 3 },
|
||||
values:
|
||||
'id,code,cycle,to_date,from_date,amount,paid_amount,amount_remain,remain_amount,status,status__name,txn_detail,txn_detail__transaction,txn_detail__phase,txn_detail__amount_received',
|
||||
sort: 'txn_detail__transaction,cycle'
|
||||
}
|
||||
"id,code,cycle,to_date,from_date,amount,paid_amount,amount_remain,remain_amount,status,status__name,txn_detail,txn_detail__transaction,txn_detail__phase,txn_detail__amount_received",
|
||||
sort: "txn_detail__transaction,cycle",
|
||||
};
|
||||
|
||||
const ttthnvConn = $copy($findapi('reservation'))
|
||||
const ttthnvConn = $copy($findapi("reservation"));
|
||||
ttthnvConn.params = {
|
||||
filter: { transaction__phase: 3, phase: 4 },
|
||||
values: 'id,transaction,phase,amount,amount_received,amount_remaining,status',
|
||||
sort: 'transaction'
|
||||
}
|
||||
values: "id,transaction,phase,amount,amount_received,amount_remaining,status",
|
||||
sort: "transaction",
|
||||
};
|
||||
|
||||
const [txnRs, detailRs, schRs, ttthnvRs] = await $getapi([
|
||||
txnConn,
|
||||
detailConn,
|
||||
schConn,
|
||||
ttthnvConn
|
||||
])
|
||||
const [txnRs, detailRs, schRs, ttthnvRs] = await $getapi([txnConn, detailConn, schConn, ttthnvConn]);
|
||||
|
||||
const transactions = txnRs?.data?.rows || []
|
||||
const details = detailRs?.data?.rows || []
|
||||
const schedules = schRs?.data?.rows || []
|
||||
const ttthnvList = ttthnvRs?.data?.rows || []
|
||||
const transactions = txnRs?.data?.rows || [];
|
||||
const details = detailRs?.data?.rows || [];
|
||||
const schedules = schRs?.data?.rows || [];
|
||||
const ttthnvList = ttthnvRs?.data?.rows || [];
|
||||
|
||||
if (!transactions.length) {
|
||||
loading.value = false
|
||||
return
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// TTTHNV map
|
||||
const ttthnvMap = {}
|
||||
ttthnvList.forEach(t => {
|
||||
const tid = t.transaction
|
||||
ttthnvMap[tid] = (ttthnvMap[tid] || 0) + Number(t.amount_received || 0)
|
||||
})
|
||||
const ttthnvMap = {};
|
||||
ttthnvList.forEach((t) => {
|
||||
const tid = t.transaction;
|
||||
ttthnvMap[tid] = (ttthnvMap[tid] || 0) + Number(t.amount_received || 0);
|
||||
});
|
||||
|
||||
// Group schedules by transaction
|
||||
const schByTxn = {}
|
||||
schedules.forEach(s => {
|
||||
const tid = s.txn_detail__transaction
|
||||
if (!schByTxn[tid]) schByTxn[tid] = []
|
||||
schByTxn[tid].push(s)
|
||||
})
|
||||
const schByTxn = {};
|
||||
schedules.forEach((s) => {
|
||||
const tid = s.txn_detail__transaction;
|
||||
if (!schByTxn[tid]) schByTxn[tid] = [];
|
||||
schByTxn[tid].push(s);
|
||||
});
|
||||
|
||||
// Tìm số đợt tối đa
|
||||
let maxCycles = 0
|
||||
Object.values(schByTxn).forEach(list => {
|
||||
const paymentList = list.filter(s => Number(s.cycle) > 0)
|
||||
if (paymentList.length > maxCycles) maxCycles = paymentList.length
|
||||
})
|
||||
let maxCycles = 0;
|
||||
Object.values(schByTxn).forEach((list) => {
|
||||
const paymentList = list.filter((s) => Number(s.cycle) > 0);
|
||||
if (paymentList.length > maxCycles) maxCycles = paymentList.length;
|
||||
});
|
||||
|
||||
scheduleHeaders.value = Array.from({ length: maxCycles }, (_, i) => ({
|
||||
label: `L0${i + 1}`,
|
||||
index: i
|
||||
}))
|
||||
index: i,
|
||||
}));
|
||||
|
||||
rows.value = transactions.map(txn => {
|
||||
rows.value = transactions.map((txn) => {
|
||||
const txnSchedules = (schByTxn[txn.id] || [])
|
||||
.filter(s => Number(s.cycle) > 0)
|
||||
.sort((a, b) => Number(a.cycle) - Number(b.cycle))
|
||||
.filter((s) => Number(s.cycle) > 0)
|
||||
.sort((a, b) => Number(a.cycle) - Number(b.cycle));
|
||||
|
||||
const ttthnvPaid = ttthnvMap[txn.id] || 0
|
||||
const salePriceNum = Number(txn.sale_price || 0)
|
||||
const ttthnvPaid = ttthnvMap[txn.id] || 0;
|
||||
const salePriceNum = Number(txn.sale_price || 0);
|
||||
|
||||
// ───────────────────────────────────────────────
|
||||
// QUAN TRỌNG: Theo yêu cầu mới nhất của bạn
|
||||
// Lũy kế HĐCN = TTTHNV
|
||||
const luyKe = ttthnvPaid
|
||||
const luyKe = ttthnvPaid;
|
||||
// ───────────────────────────────────────────────
|
||||
|
||||
// Phân bổ TTTHNV dần vào từng đợt → tính lũy kế sang đợt
|
||||
let remainingTTTHNV = ttthnvPaid
|
||||
let remainingTTTHNV = ttthnvPaid;
|
||||
|
||||
const schedulesWithCalc = txnSchedules.map(sch => {
|
||||
const scheduleAmount = Number(sch.amount || 0)
|
||||
const schedulesWithCalc = txnSchedules.map((sch) => {
|
||||
const scheduleAmount = Number(sch.amount || 0);
|
||||
|
||||
// Lũy kế sang đợt = min(remaining TTTHNV, số tiền đợt)
|
||||
const luyKeSangDot = Math.min(remainingTTTHNV, scheduleAmount)
|
||||
const luyKeSangDot = Math.min(remainingTTTHNV, scheduleAmount);
|
||||
|
||||
// Số tiền đã thực thanh toán = paid_amount - lũy kế sang đợt
|
||||
const paidAmountFromSchedule = Number(sch.paid_amount || 0)
|
||||
const thucThanhToan = Math.max(0, paidAmountFromSchedule - luyKeSangDot)
|
||||
const paidAmountFromSchedule = Number(sch.paid_amount || 0);
|
||||
const thucThanhToan = Math.max(0, paidAmountFromSchedule - luyKeSangDot);
|
||||
|
||||
// Dư nợ = số tiền đợt - lũy kế sang đợt - thực thanh toán
|
||||
const amountRemain = Math.max(0, scheduleAmount - luyKeSangDot - thucThanhToan)
|
||||
const amountRemain = Math.max(0, scheduleAmount - luyKeSangDot - thucThanhToan);
|
||||
|
||||
remainingTTTHNV -= luyKeSangDot
|
||||
remainingTTTHNV = Math.max(0, remainingTTTHNV)
|
||||
remainingTTTHNV -= luyKeSangDot;
|
||||
remainingTTTHNV = Math.max(0, remainingTTTHNV);
|
||||
|
||||
return {
|
||||
to_date: sch.to_date ? $dayjs(sch.to_date).format('DD/MM/YYYY') : '—',
|
||||
to_date: sch.to_date ? $dayjs(sch.to_date).format("DD/MM/YYYY") : "—",
|
||||
amount: scheduleAmount,
|
||||
luy_ke_sang_dot: luyKeSangDot,
|
||||
thuc_thanh_toan: thucThanhToan,
|
||||
amount_remain: amountRemain,
|
||||
status: sch.status
|
||||
}
|
||||
})
|
||||
status: sch.status,
|
||||
};
|
||||
});
|
||||
|
||||
// Tính quá hạn
|
||||
const todayDate = new Date()
|
||||
const todayDate = new Date();
|
||||
const overdue = txnSchedules.reduce((sum, sch, idx) => {
|
||||
const toDate = sch.to_date ? new Date(sch.to_date) : null
|
||||
const remain = schedulesWithCalc[idx]?.amount_remain || 0
|
||||
const toDate = sch.to_date ? new Date(sch.to_date) : null;
|
||||
const remain = schedulesWithCalc[idx]?.amount_remain || 0;
|
||||
if (toDate && toDate < todayDate && remain > 0) {
|
||||
return sum + remain
|
||||
return sum + remain;
|
||||
}
|
||||
return sum
|
||||
}, 0)
|
||||
return sum;
|
||||
}, 0);
|
||||
|
||||
const paddedSchedules = Array.from({ length: maxCycles }, (_, i) => schedulesWithCalc[i] || null)
|
||||
const paddedSchedules = Array.from({ length: maxCycles }, (_, i) => schedulesWithCalc[i] || null);
|
||||
|
||||
return {
|
||||
customer_code: txn.customer__code || '',
|
||||
customer_name: txn.customer__fullname || '',
|
||||
trade_code: txn.product__trade_code || txn.code || '',
|
||||
contract_date: txn.date ? $dayjs(txn.date).format('DD/MM/YYYY') : '—',
|
||||
customer_code: txn.customer__code || "",
|
||||
customer_name: txn.customer__fullname || "",
|
||||
trade_code: txn.product__trade_code || txn.code || "",
|
||||
contract_date: txn.date ? $dayjs(txn.date).format("DD/MM/YYYY") : "—",
|
||||
contract_date_raw: txn.date,
|
||||
sale_price: salePriceNum,
|
||||
ttthnv_paid: ttthnvPaid,
|
||||
luy_ke: luyKe, // ← chính là TTTHNV
|
||||
luy_ke: luyKe, // ← chính là TTTHNV
|
||||
schedules: paddedSchedules,
|
||||
overdue: overdue
|
||||
}
|
||||
})
|
||||
overdue: overdue,
|
||||
};
|
||||
});
|
||||
|
||||
filteredRows.value = rows.value
|
||||
filteredRows.value = rows.value;
|
||||
} catch (e) {
|
||||
console.error('BaoCaoCongNo error:', e)
|
||||
console.error("BaoCaoCongNo error:", e);
|
||||
} finally {
|
||||
loading.value = false
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function fmt(val) {
|
||||
const n = Number(val)
|
||||
if (isNaN(n) || (!n && n !== 0)) return '—'
|
||||
return n.toLocaleString('vi-VN')
|
||||
const n = Number(val);
|
||||
if (isNaN(n) || (!n && n !== 0)) return "—";
|
||||
return n.toLocaleString("vi-VN");
|
||||
}
|
||||
|
||||
function pct(num, denom) {
|
||||
const n = Number(num)
|
||||
const d = Number(denom)
|
||||
if (!d || isNaN(n)) return '—'
|
||||
return (n / d * 100).toFixed(1) + '%'
|
||||
const n = Number(num);
|
||||
const d = Number(denom);
|
||||
if (!d || isNaN(n)) return "—";
|
||||
return ((n / d) * 100).toFixed(1) + "%";
|
||||
}
|
||||
|
||||
function pctClass(paid, amount) {
|
||||
const p = Number(paid)
|
||||
const a = Number(amount)
|
||||
if (isNaN(p) || isNaN(a) || !a) return ''
|
||||
const ratio = p / a
|
||||
if (ratio >= 1) return 'has-text-success'
|
||||
if (ratio >= 0.5) return 'has-text-info'
|
||||
return 'has-text-danger'
|
||||
const p = Number(paid);
|
||||
const a = Number(amount);
|
||||
if (isNaN(p) || isNaN(a) || !a) return "";
|
||||
const ratio = p / a;
|
||||
if (ratio >= 1) return "has-text-success";
|
||||
if (ratio >= 0.5) return "has-text-info";
|
||||
return "has-text-danger";
|
||||
}
|
||||
|
||||
function colSum(scheduleIndex, field) {
|
||||
return filteredRows.value.reduce((sum, row) => {
|
||||
const sch = row.schedules[scheduleIndex]
|
||||
return sum + (sch ? Number(sch[field] || 0) : 0)
|
||||
}, 0)
|
||||
const sch = row.schedules[scheduleIndex];
|
||||
return sum + (sch ? Number(sch[field] || 0) : 0);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
const totalOverdue = computed(() =>
|
||||
filteredRows.value.reduce((s, r) => s + Number(r.overdue || 0), 0)
|
||||
)
|
||||
const totalOverdue = computed(() => filteredRows.value.reduce((s, r) => s + Number(r.overdue || 0), 0));
|
||||
|
||||
function exportExcel() {
|
||||
const headers = [
|
||||
'STT',
|
||||
'Mã KH',
|
||||
'Mã Căn',
|
||||
'Ngày ký HĐ',
|
||||
'Giá trị HĐMB',
|
||||
'Tiền nộp TTTHNV',
|
||||
'Lũy kế tiền về HĐCN',
|
||||
'Tỷ lệ HĐCN'
|
||||
]
|
||||
"STT",
|
||||
"Mã KH",
|
||||
"Mã Căn",
|
||||
"Ngày ký HĐ",
|
||||
"Giá trị HĐMB",
|
||||
"Tiền nộp TTTHNV",
|
||||
"Lũy kế tiền về HĐCN",
|
||||
"Tỷ lệ HĐCN",
|
||||
];
|
||||
|
||||
scheduleHeaders.value.forEach(h => {
|
||||
scheduleHeaders.value.forEach((h) => {
|
||||
headers.push(
|
||||
`${h.label} - Ngày`,
|
||||
`${h.label} - Số tiền`,
|
||||
`${h.label} - Lũy kế sang`,
|
||||
`${h.label} - Số tiền đã thực thanh toán`,
|
||||
`${h.label} - Tỷ lệ`,
|
||||
`${h.label} - Dư nợ`
|
||||
)
|
||||
})
|
||||
`${h.label} - Dư nợ`,
|
||||
);
|
||||
});
|
||||
|
||||
headers.push('Số tiền quá hạn')
|
||||
headers.push("Số tiền quá hạn");
|
||||
|
||||
const data = filteredRows.value.map((row, i) => {
|
||||
const base = [
|
||||
@@ -455,11 +517,11 @@ function exportExcel() {
|
||||
fmt(row.sale_price),
|
||||
fmt(row.ttthnv_paid),
|
||||
fmt(row.luy_ke),
|
||||
pct(row.luy_ke, row.sale_price)
|
||||
]
|
||||
pct(row.luy_ke, row.sale_price),
|
||||
];
|
||||
|
||||
scheduleHeaders.value.forEach((_, si) => {
|
||||
const sch = row.schedules[si]
|
||||
const sch = row.schedules[si];
|
||||
if (sch) {
|
||||
base.push(
|
||||
sch.to_date,
|
||||
@@ -467,31 +529,31 @@ function exportExcel() {
|
||||
fmt(sch.luy_ke_sang_dot),
|
||||
fmt(sch.thuc_thanh_toan),
|
||||
pct(sch.thuc_thanh_toan, sch.amount),
|
||||
fmt(sch.amount_remain)
|
||||
)
|
||||
fmt(sch.amount_remain),
|
||||
);
|
||||
} else {
|
||||
base.push('', '', '', '', '', '')
|
||||
base.push("", "", "", "", "", "");
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
base.push(fmt(row.overdue))
|
||||
return base
|
||||
})
|
||||
base.push(fmt(row.overdue));
|
||||
return base;
|
||||
});
|
||||
|
||||
const csvRows = [headers, ...data]
|
||||
const csv = csvRows.map(r => r.map(c => `"${String(c ?? '').replace(/"/g, '""')}"`).join(',')).join('\n')
|
||||
const csvRows = [headers, ...data];
|
||||
const csv = csvRows.map((r) => r.map((c) => `"${String(c ?? "").replace(/"/g, '""')}"`).join(",")).join("\n");
|
||||
|
||||
const BOM = '\uFEFF'
|
||||
const blob = new Blob([BOM + csv], { type: 'text/csv;charset=utf-8;' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `bao-cao-cong-no-${$dayjs().format('YYYYMMDD')}.csv`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
const BOM = "\uFEFF";
|
||||
const blob = new Blob([BOM + csv], { type: "text/csv;charset=utf-8;" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `bao-cao-cong-no-${$dayjs().format("YYYYMMDD")}.csv`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
onMounted(() => loadData())
|
||||
onMounted(() => loadData());
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -578,4 +640,4 @@ onMounted(() => loadData())
|
||||
.fixed-col {
|
||||
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user