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

1198 lines
36 KiB
Vue

<template>
<div class="report-daily-page mb-4">
<!-- Report product section -->
<div class="header-report mb-2">
<h2 class="header-report-title">Báo cáo tình hình giao dịch & thanh toán theo giai đoạn</h2>
<div class="select-date">
<span class="label-date">Từ ngày:</span> &nbsp;
<Datepicker v-bind="{ record: dateReport, attr: 'date-start' }" @date="select('date-start', $event)" />&nbsp;
<span class="label-date">đến ngày:</span> &nbsp;
<Datepicker v-bind="{ record: dateReport, attr: 'date-end' }" @date="select('date-end', $event)" />
</div>
</div>
<div class="body-report">
<div class="general-report mb-4">
<!-- Transaction -->
<div class="general-report-transaction mb-4">
<h3 class="general-report-title">Giao dịch</h3>
<div class="grid">
<div class="cell">
<div class="card">
<div class="card-content">
<p>Tổng doanh thu</p>
<span>{{$numtoString(revenueSummary.salePrice, { hasUnit: true }) }}</span>
</div>
</div>
</div>
<div class="cell">
<div class="card">
<div class="card-content">
<p>Đã thu</p>
<span>{{$numtoString(revenueSummary.depositAmount, { hasUnit: true }) }}</span>
</div>
</div>
</div>
<div class="cell">
<div class="card">
<div class="card-content">
<p>Còn chờ thu</p>
<span>{{$numtoString(revenueSummary.salePrice - revenueSummary.depositAmount, { hasUnit: true }) }}</span>
</div>
</div>
</div>
<div class="cell">
<div class="card">
<div class="card-content">
<p>Tỉ lệ thu (%)</p>
<span
:style="{
color: ColorStatus(calcPercentage(revenueSummary.depositAmount, revenueSummary.salePrice)),
}"
>{{ calcPercentage(revenueSummary.depositAmount, revenueSummary.salePrice) }} %</span
>
</div>
</div>
</div>
</div>
</div>
<!-- Payment -->
<div class="general-report-payment mb-4">
<h3 class="general-report-title">Thanh toán</h3>
<div class="grid">
<div class="cell">
<div class="card">
<div class="card-content">
<p>Tổng tiền phải thu</p>
<span>{{$numtoString(paymentStatusSummary.totalPay, { hasUnit: true }) }}</span>
</div>
</div>
</div>
<div class="cell">
<div class="card">
<div class="card-content">
<p>Đã thu</p>
<span>{{$numtoString(paymentStatusSummary.paid, { hasUnit: true }) }}</span>
</div>
</div>
</div>
<div class="cell">
<div class="card">
<div class="card-content">
<p>Còn phải thu</p>
<span>{{$numtoString(paymentStatusSummary.unpaid, { hasUnit: true }) }}</span>
</div>
</div>
</div>
<div class="cell">
<div class="card">
<div class="card-content">
<p>Tỉ lệ hoàn thành (%)</p>
<span
:style="{
color: ColorStatus(calcPercentage(paymentStatusSummary.paid, paymentStatusSummary.totalPay)),
}"
>{{ calcPercentage(paymentStatusSummary.paid, paymentStatusSummary.totalPay) }} %</span
>
</div>
</div>
</div>
</div>
</div>
<!-- Due -->
<div class="general-report-due">
<h3 class="general-report-title">Đến hạn</h3>
<div class="grid">
<div class="cell">
<div class="card">
<div class="card-content">
<p>Giao dịch đến hạn</p>
<span>{{ totalTransactionDue }}</span>
</div>
</div>
</div>
<div class="cell">
<div class="card">
<div class="card-content">
<p>Doanh thu từ giao dịch đến hạn</p>
<span>{{$numtoString(totalAmountTransactionDue, { hasUnit: true }) }}</span>
</div>
</div>
</div>
<div class="cell">
<div class="card">
<div class="card-content">
<p>Thanh toán đến hạn</p>
<span>{{ totalPaymentDue }}</span>
</div>
</div>
</div>
<div class="cell">
<div class="card">
<div class="card-content">
<p>Doanh thu thanh toán đến hạn</p>
<span>{{$numtoString(totalAmountPaymentDue, { hasUnit: true }) }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="report report-transaction">
<h3>Giao dịch</h3>
<div class="report-transaction-new">
<div class="grid">
<div
class="cell"
v-for="item in dataTransactionType"
:key="item.code"
@click="handlerTransaction($event, item)"
>
<div class="card">
<div class="card-content">
<p>{{ item.name }}</p>
<span>{{ item.count | 0 }}</span>
</div>
</div>
</div>
</div>
<div class="chart">
<client-only v-if="hasTransactionDataNews">
<highcharts-vue-chart :highcharts="Highcharts" :options="chartTransactionOptions" />
</client-only>
<p v-else class="message-alter">{{ emptyMessageTransactionNews }}</p>
</div>
</div>
<!-- Dealer transactions -->
<div class="chart dealer-transactions">
<client-only>
<highcharts-vue-chart :highcharts="Highcharts" :options="chartDealerTransactionOptions" />
</client-only>
</div>
</div>
<div class="report report-payment">
<h3>Thanh toán</h3>
<div class="report-payment-new">
<div class="grid">
<div
class="cell"
v-for="item in paymentTypeStatus"
:key="item.code"
@click="handlerPaymentSelect($event, item)"
>
<div class="card">
<div class="card-content">
<p>{{ item.name }}</p>
<span>{{ item.count | 0 }}</span>
</div>
</div>
</div>
</div>
<div class="chart">
<client-only v-if="hasPaymentDataNews">
<highcharts-vue-chart :highcharts="Highcharts" :options="chartPaymentOptions" />
</client-only>
<p v-else class="message-alter">{{ emptyMessagePayMentNews }}</p>
</div>
</div>
</div>
</div>
<!-- Popup -->
<div :class="['modal', { 'is-active': showPopup }]">
<div class="modal-background" @click="handlerClosePopup"></div>
<div class="modal-card">
<header class="modal-card-head my-0 py-2" v-if="titlePopup">
<div style="width: 100%">
<div class="field is-grouped">
<div class="control is-expanded has-text-left">
<p class="fsb-18 has-text-primary" v-html="titlePopup"></p>
</div>
<div class="control has-text-right">
<button class="delete is-medium" @click="handlerClosePopup"></button>
</div>
</div>
</div>
</header>
<div class="modal-card-body px-4 py-4">Load data danh sách sản phẩm theo trạng thái</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from "vue";
import Highcharts from "highcharts";
import { Chart as HighchartsVueChart } from "highcharts-vue";
import Datepicker from "~/components/datepicker/Datepicker";
const { $numtoString, $findapi, $getapi, $store, $getdata, $mode } = useNuxtApp();
const dealer = $store.dealer;
const isVietnamese = computed(() => $store.lang.toLowerCase() === "vi");
const today = new Date().toISOString().split("T")[0];
const productsResponse = ref(null);
const transactionResponse = ref(null);
const transactionDueResponse = ref(null);
const paymentResponse = ref(null);
const paymentDueResponse = ref(null);
const showPopup = ref(false);
const dateReport = ref({
"date-start": today,
"date-end": today,
});
const labelProductByStatus = ref(null);
const labelTransactionPhase = ref(null);
const productByStatus = ref(null);
const dataTransactionSelect = ref(null);
const dataPaymentSelect = ref(null);
const labelPaymentType = ref(null);
const selectedProductByStatus = null;
const totalProduct = ref(0);
const totalTransactionNews = ref(0);
const totalTransactionDue = ref(0);
const totalPaymentNews = ref(0);
const totalPaymentDue = ref(0);
const listDealer = ref(null);
const formatVniDate = (date) => {
const dateTemp = date ? new Date(date) : new Date();
const d = String(dateTemp.getDate()).padStart(2, "0");
const m = String(dateTemp.getMonth() + 1).padStart(2, "0");
const y = dateTemp.getFullYear();
return `${d}/${m}/${y}`;
};
let scrollPosition = 0;
const select = (fieldName, value) => {
dateReport.value[fieldName] = value;
};
const listTransactionPhase = await $getdata("transactionphase");
const listSalePolicy = await $getdata("salepolicy");
const listPaymentStatus = await $getdata("paymentstatus");
const listPaymentType = await $getdata("paymenttype");
if (!dealer?.code) {
listDealer.value = await $getdata("dealer");
}
const groupByField = (list, { keyField, nameField, nameTransform, colorField }) => {
if (!Array.isArray(list) || list.length === 0) return [];
const map = new Map();
for (const item of list) {
const key = item[keyField];
if (!key) continue;
let group = map.get(key);
if (!group) {
group = {
key,
name: nameTransform ? nameTransform(item[nameField]) : nameField ? item[nameField] : key,
color: colorField ? item[colorField] : null,
count: 0,
children: [],
};
map.set(key, group);
}
group.count++;
group.children.push(item);
}
return [...map.values()];
};
const chartSelectedHandler = ({ source, labelRef, itemRef, selectRef }) => ({
select() {
selectRef = this;
const item = source.value.find((i) => i.key === this.options.key);
labelRef.value = item?.name ?? null;
itemRef.value = item?.children ? [...item.children] : [];
toggleBodyScroll(true);
},
unselect() {
if (selectRef !== this) return;
selectRef = null;
labelRef.value = null;
itemRef.value = [];
toggleBodyScroll(false);
},
});
const toggleBodyScroll = (isActive) => {
if (isActive) {
scrollPosition = window.scrollY || document.documentElement.scrollTop;
document.body.classList.add("popup-open");
document.body.style.top = `-${scrollPosition}px`;
} else {
document.body.classList.remove("popup-open");
document.body.style.top = "";
window.scrollTo(0, scrollPosition);
}
};
function buildDateRange(date) {
if (!date) return null;
const dateTemp = new Date(date).toISOString().split("T")[0];
const start = new Date(`${dateTemp}T00:00:00.000Z`);
const end = new Date(`${dateTemp}T23:59:59.999Z`);
return {
start: start.toISOString(),
end: end.toISOString(),
};
}
function buildGroupMap(list = []) {
if (!Array.isArray(list)) return new Map();
return new Map(list.filter((item) => item?.key != null).map((item) => [item.key, item]));
}
function mergeMasterWithGroup(masterList = [], groupMap) {
if (!Array.isArray(masterList)) return [];
return masterList.map((item) => {
const found = groupMap.get(item.code);
return {
...item,
count: found?.count ?? 0,
children: found?.children ?? [],
};
});
}
function buildStatusByParent(parentList = [], statusList = [], keyGroup = "") {
return parentList.map((parent) => {
const groupedStatus =
groupByField(parent.children ?? [], {
keyField: keyGroup,
}) || [];
const statusMap = buildGroupMap(groupedStatus);
const status = statusList.map((st) => {
const found = statusMap.get(st.code);
return {
...st,
count: found?.count ?? 0,
children: found?.children ?? [],
};
});
return {
...parent,
status,
};
});
}
function buildChartSeriesByStatus(statusList = [], parentWithStatus = []) {
return statusList.map((status) => ({
name: status.name?.split(". ")[1] ?? status.name,
data: parentWithStatus.map((parent) => {
const map = new Map((parent.status ?? []).map((st) => [st.code, st.count]));
return map.get(status.code) ?? 0;
}),
}));
}
function calcPercentage(part, whole, decimals = 2) {
const numerator = Number(part) || 0;
const denominator = Number(whole) || 0;
if (denominator === 0) return 0;
return Number(((numerator / denominator) * 100).toFixed(decimals));
}
function hasData(data) {
return Array.isArray(data) && data.length > 0;
}
function ColorStatus(value) {
if (value > 100) {
return "#006400";
}
if (value >= 90 && value <= 100) {
return "#2ECC71";
}
if (value >= 75 && value < 90) {
return "#F1C40F";
}
if (value >= 60 && value < 75) {
return "#E67E22";
}
if (value < 60) {
return "#E74C3C";
}
return "#BDC3C7";
}
const titlePopup = computed(() => {
const isVi = isVietnamese;
const productLen = productByStatus?.value?.length;
const transactionLen = dataTransactionSelect?.value?.length;
const paymentLen = dataPaymentSelect?.value?.length;
const dateStart = dateReport.value?.["date-start"];
const dateEnd = dateReport.value?.["date-end"];
const dateTextStart = dateStart ? formatVniDate(dateStart) : "";
const dateTextEnd = dateEnd ? formatVniDate(dateEnd) : "";
if (productLen) {
return isVi
? `Danh sách sản phẩm - Trạng thái: ${labelProductByStatus.value}`
: `Product List - Status: ${labelProductByStatus.value}`;
}
if (transactionLen) {
return isVi
? `Danh sách giao dịch - Trạng thái: ${labelTransactionPhase.value} (${dateTextStart} - ${dateTextEnd})`
: `Transaction list - Status: ${labelTransactionPhase.value} (${dateTextStart} - ${dateTextEnd})`;
}
if (paymentLen) {
return isVi
? `Danh sách thanh toán - Loại thanh toán: ${labelPaymentType.value} (${dateTextStart} - ${dateTextEnd})`
: `Payment list - Payment type: ${labelPaymentType.value} (${dateTextStart} - ${dateTextEnd})`;
}
return "";
});
// Fetch products data
let foundProduct = $findapi("product");
foundProduct.params.values =
"id,code,trade_code,zone_code,type__code,type__name,zone_type__code,zone_type__code,zone_type__name,direction__code,direction__name,cart__code,cart__name,cart__dealer__code,cart__dealer__name,origin_price,status__code,status__name,status__color,status__sale_status__code,status__sale_status__name,status__sale_status__color";
if (dealer?.code) {
foundProduct.params.filter = { cart__dealer__code: dealer?.code };
foundProduct.params.exclude = { status__sale_status__code: "not-sold" };
}
async function fetchApiProduct() {
try {
const [productRes] = await $getapi([foundProduct]);
productsResponse.value = productRes?.data?.rows ?? [];
totalProduct.value = productsResponse.value?.length;
} catch (error) {
if ($mode === "dev") {
console.error("Call api product error", error);
}
productsResponse.value = [];
totalProduct.value = 0;
}
}
fetchApiProduct();
const hasProducts = computed(() => hasData(productsResponse.value));
const emptyMessageProducts = computed(() => (isVietnamese ? `Hiện chưa có sản phẩm` : `No product`));
// Chart product by status
const productStatus = computed(() =>
groupByField(productsResponse.value, {
keyField: "status__sale_status__code",
nameField: "status__sale_status__name",
nameTransform: (name) => name?.split(". ")[1] ?? name,
colorField: "status__sale_status__color",
})
);
const chartProductStatusOptions = computed(() => {
return {
chart: {
type: "pie",
zooming: {
type: "xy",
},
panning: {
enabled: true,
type: "xy",
},
},
title: {
text: isVietnamese ? "Trạng thái sản phẩm" : "Product status",
},
subtitle: {
text: `(${totalProduct.value} ${isVietnamese ? "Sản phẩm" : "Products"})`,
},
plotOptions: {
pie: {
allowPointSelect: true,
cursor: "pointer",
point: {
events: chartSelectedHandler({
source: productStatus,
labelRef: labelProductByStatus,
itemRef: productByStatus,
selectRef: selectedProductByStatus,
}),
},
},
},
series: [
{
name: isVietnamese ? "Số lượng" : "Quantity",
colorByPoint: true,
data: productStatus.value.map((item) => {
return {
name: item.name,
y: item.count,
key: item.key,
color: item.color,
};
}),
},
],
};
});
watch(
productByStatus,
(newVal) => {
showPopup.value = Array.isArray(newVal) ? newVal.length > 0 : !!newVal;
},
{ deep: true }
);
// <-- End Chart product by status -->
// Fetch transactions data
let foundTransaction = $findapi("reservation");
foundTransaction.params.values =
"id,code,transaction__code,transaction__product__trade_code,transaction__product__zone_code,transaction__customer__code,transaction__customer__fullname,transaction__policy__code,transaction__policy__name,phase,phase__code,phase__name,transaction__product__cart__code,transaction__product__cart__name,transaction__product__cart__dealer,transaction__product__cart__dealer__code,transaction__product__cart__dealer__name,transaction__origin_price,transaction__discount_amount,transaction__sale_price,transaction__deposit_amount,status,status__code,status__name,create_time";
foundTransaction.params.filter = {
...(dealer?.code
? {
transaction__product__cart__dealer__code: dealer?.code,
}
: {}),
create_time__gte: buildDateRange(dateReport.value["date-start"])?.start,
create_time__lte: buildDateRange(dateReport.value["date-end"])?.end,
};
let foundTransactionDue = $findapi("reservation");
foundTransactionDue.params.values =
"id,code,due_date,transaction__code,transaction__product__trade_code,transaction__product__zone_code,transaction__customer__code,transaction__customer__fullname,transaction__policy__code,transaction__policy__name,phase,phase__code,phase__name,transaction__product__cart__code,transaction__product__cart__name,transaction__product__cart__dealer,transaction__product__cart__dealer__code,transaction__product__cart__dealer__name,transaction__origin_price,transaction__discount_amount,transaction__sale_price,transaction__deposit_amount,status,status__code,status__name,create_time";
foundTransactionDue.params.filter = {
...(dealer?.code
? {
transaction__product__cart__dealer__code: dealer?.code,
}
: {}),
due_date__gte: dateReport.value["date-start"],
due_date__lte: dateReport.value["date-end"],
};
foundTransactionDue.params.exclude = { status__code__in: ["approved", "reject", "cancel"] };
async function fetchApiTransaction() {
try {
const [transactionRes, transactionDueRes] = await $getapi([foundTransaction, foundTransactionDue]);
const transactions = transactionRes?.data?.rows ?? [];
const dueTransactions = transactionDueRes?.data?.rows ?? [];
transactionResponse.value = transactions;
totalTransactionNews.value = transactions.length;
transactionDueResponse.value = dueTransactions;
totalTransactionDue.value = dueTransactions.length;
} catch (error) {
if ($mode === "dev") {
console.error("Call api product error", error);
}
transactionResponse.value = [];
totalTransactionNews.value = 0;
transactionDueResponse.value = [];
totalTransactionDue.value = 0;
}
}
fetchApiTransaction();
// <-- End Fetch transactions data -->
const transactionType = computed(() =>
groupByField(transactionResponse.value || [], {
keyField: "phase__code",
nameField: "phase__name",
nameTransform: (name) => name?.split(". ")[1] ?? name,
})
);
const revenueSummary = computed(() => {
const list = transactionResponse.value;
if (!Array.isArray(list) || list.length === 0) {
return {
originPrice: 0,
discountAmount: 0,
salePrice: 0,
depositAmount: 0,
};
}
return list.reduce(
(acc, item) => {
acc.originPrice += Number(item.transaction__origin_price) || 0;
acc.discountAmount += Number(item.transaction__discount_amount) || 0;
acc.salePrice += Number(item.transaction__sale_price) || 0;
acc.depositAmount += Number(item.transaction__deposit_amount) || 0;
return acc;
},
{
originPrice: 0,
discountAmount: 0,
salePrice: 0,
depositAmount: 0,
}
);
});
const transactionTypeGroupMap = computed(() => buildGroupMap(transactionType.value));
const dataTransactionType = computed(() => mergeMasterWithGroup(listTransactionPhase, transactionTypeGroupMap.value));
const transactionTypePolicy = computed(() =>
buildStatusByParent(transactionType.value, listSalePolicy, "transaction__policy__code")
);
const dataChartTransactionPolicy = computed(() =>
buildChartSeriesByStatus(listSalePolicy, transactionTypePolicy.value)
);
const chartTransactionOptions = computed(() => {
return {
chart: {
type: "column",
},
title: {
text: `${
isVietnamese ? "Giao dịch phát sinh giai đoạn" : "Transactions occur during this period."
} ${formatVniDate(dateReport.value?.["date-start"])} - ${formatVniDate(dateReport.value?.["date-end"])}`,
},
subtitle: {
text: `${totalTransactionNews.value} ${isVietnamese ? "Giao dịch" : "Transaction"}`,
},
xAxis: {
categories: dataTransactionType?.value.map((item) => item.name),
crosshair: true,
accessibility: {
description: isVietnamese ? "Giao đoạn giao dịch" : "Trading phase",
},
},
yAxis: {
min: 0,
title: {
text: isVietnamese ? "Số lượng giao dịch" : "Number of transactions",
},
},
tooltip: {
valueSuffix: isVietnamese ? " (Giao dịch)" : " (Transaction)",
},
plotOptions: {
column: {
pointPadding: 0.2,
borderWidth: 0,
},
},
series: dataChartTransactionPolicy.value,
};
});
const hasTransactionDataNews = computed(() => hasData(transactionResponse.value));
const emptyMessageTransactionNews = computed(() =>
isVietnamese
? `Hiện chưa có giao dịch phát sinh giai đoạn ${formatVniDate(dateReport.value["date-start"])} - ${formatVniDate(
dateReport.value["date-end"]
)}`
: `No transactions phase ${formatVniDate(dateReport.value["date-start"])} - ${formatVniDate(
dateReport.value["date-end"]
)}`
);
const handlerTransaction = (e, data) => {
e.preventDefault();
e.stopPropagation();
toggleBodyScroll(true);
showPopup.value = true;
dataTransactionSelect.value = data.children ?? [];
labelTransactionPhase.value = data.name ?? "";
};
// Dealer Transaction
const transactionDealer = computed(() => {
if (dealer?.code) return [];
const test = groupByField(transactionResponse.value || [], {
keyField: "transaction__product__cart__dealer__code",
nameField: "transaction__product__cart__dealer__name",
nameTransform: (name) => name?.split(". ")[1] ?? name,
});
console.log('transactionDealer==================' ,test);
return test
});
const transactionDealerGroupMap = computed(() => buildGroupMap(transactionDealer.value));
const dataTransactionDealer = computed(() => {
if (dealer?.code) return [];
const test = mergeMasterWithGroup(listDealer.value || [], transactionDealerGroupMap.value);
console.log('test' , test);
return test
});
const chartDealerTransactionOptions = computed(() => {
return {
chart: {
type: "column",
},
title: {
text: `${
isVietnamese
? "Giao dịch phát sinh theo đại lý trong giai đoạn"
: "Transactions are generated through phased agency"
} ${formatVniDate(dateReport.value?.["date-start"])} - ${formatVniDate(dateReport.value?.["date-end"])}`,
},
subtitle: {
text: `${totalTransactionNews.value} ${isVietnamese ? "Giao dịch" : "Transaction"}`,
},
xAxis: {
categories: listDealer.value?.map((d) => d.code),
},
yAxis: {
min: 0,
title: {
text: isVietnamese ? "Phần trăm (%)" : "Percent (%)",
},
},
tooltip: {
pointFormat:
'<span style="color:{series.color}">{series.name}</span>' + ": <b>{point.y}</b> ({point.percentage:.0f}%)<br/>",
shared: true,
},
plotOptions: {
column: {
stacking: "percent",
dataLabels: {
enabled: true,
format: "{point.percentage:.0f}%",
},
},
},
series: [
{
name: "Đặt cọc",
data: [434, 290, 307, 0, 0],
},
{
name: "Giữ chỗ",
data: [272, 153, 156, 0, 0],
},
{
name: "Thỏa thuận THNV",
data: [13, 7, 8, 1, 0],
},
{
name: "Chuyển nhượng QSDĐ",
data: [55, 35, 41, 6, 0],
},
],
};
});
// Transaction Due
const totalAmountTransactionDue = computed(() => {
return transactionDueResponse.value?.reduce((total, txn) => {
const salePrice = Number(txn.transaction__sale_price) || 0;
const deposit = Number(txn.transaction__deposit_amount) || 0;
return total + (salePrice - deposit);
}, 0);
});
// <-- End Transaction Due -->
// fetch data payment
let foundPayment = $findapi("payment_schedule");
foundPayment.params.values =
"id,code,from_date,to_date,type,type__code,type__name,amount,paid_amount,remain_amount,cycle,cycle_days,detail,status,status__code,status__name,txn_detail__code,txn_detail__phase,txn_detail__phase__code,txn_detail__phase__name,txn_detail__transaction__product__trade_code,txn_detail__transaction__product__cart__dealer__code,txn_detail__transaction__product__cart__dealer__name,txn_detail__transaction__customer__code,txn_detail__transaction__customer__fullname,txn_detail__transaction__customer__code,txn_detail__transaction__customer__phone,create_time";
foundPayment.params.filter = {
...(dealer?.code
? {
txn_detail__transaction__product__cart__dealer__code: dealer?.code || null,
}
: {}),
create_time__gte: buildDateRange(dateReport.value["date-start"])?.start,
create_time__lte: buildDateRange(dateReport.value["date-end"])?.end,
};
let foundPaymentDue = $findapi("payment_schedule");
foundPaymentDue.params.values =
"id,code,from_date,to_date,type,type__code,type__name,amount,paid_amount,remain_amount,cycle,cycle_days,detail,status,status__code,status__name,txn_detail__code,txn_detail__phase,txn_detail__phase__code,txn_detail__phase__name,txn_detail__transaction__product__trade_code,txn_detail__transaction__product__cart__dealer__code,txn_detail__transaction__product__cart__dealer__name,txn_detail__transaction__customer__code,txn_detail__transaction__customer__fullname,txn_detail__transaction__customer__code,txn_detail__transaction__customer__phone,create_time";
foundPaymentDue.params.filter = {
...(dealer?.code
? {
txn_detail__transaction__product__cart__dealer__code: dealer?.code || null,
}
: {}),
to_date__gte: dateReport.value["date-start"],
to_date__lte: dateReport.value["date-end"],
status__code: "unpaid",
};
async function fetchApiPayment() {
try {
const [paymentRes, paymentDueRes] = await $getapi([foundPayment, foundPaymentDue]);
const payments = paymentRes?.data?.rows ?? [];
const duePayments = paymentDueRes?.data?.rows ?? [];
paymentResponse.value = payments;
totalPaymentNews.value = payments.length;
paymentDueResponse.value = duePayments;
totalPaymentDue.value = duePayments.length;
} catch (error) {
if ($mode === "dev") {
console.error("Call api product error", error);
}
paymentResponse.value = [];
totalPaymentNews.value = 0;
paymentDueResponse.value = [];
totalPaymentDue.value = 0;
}
}
fetchApiPayment();
const paymentTypeGroup = computed(() =>
groupByField(paymentResponse.value || [], {
keyField: "type__code",
nameField: "type__name",
nameTransform: (name) => name?.split(". ")[1] ?? name,
})
);
const paymentTypeGroupMap = computed(() => buildGroupMap(paymentTypeGroup.value));
const dataPaymentType = computed(() => mergeMasterWithGroup(listPaymentType, paymentTypeGroupMap.value));
const paymentTypeStatus = computed(() => buildStatusByParent(dataPaymentType.value, listPaymentStatus, "status__code"));
const dataChartPaymentStatus = computed(() => buildChartSeriesByStatus(listPaymentStatus, paymentTypeStatus.value));
const chartPaymentOptions = computed(() => {
return {
chart: {
type: "column",
},
title: {
text: `${isVietnamese ? "Thanh toán phát giai đoạn" : "Payment due in stages"} ${formatVniDate(
dateReport.value?.["date-start"]
)} - ${formatVniDate(dateReport.value?.["date-end"])}`,
},
subtitle: {
text: `${totalPaymentNews.value || 0} ${isVietnamese ? "Thanh toán" : "Payment"}`,
},
xAxis: {
categories: listPaymentType?.map((item) => item.name),
crosshair: true,
accessibility: {
description: isVietnamese ? "Loại thanh toán" : "Payment type",
},
},
yAxis: {
min: 0,
title: {
text: isVietnamese ? "Số lượng giao dịch" : "Number of transactions",
},
},
tooltip: {
valueSuffix: isVietnamese ? " (Giao dịch)" : " (Transaction)",
},
plotOptions: {
column: {
pointPadding: 0.2,
borderWidth: 0,
},
},
series: dataChartPaymentStatus.value,
};
});
const hasPaymentDataNews = computed(() => hasData(paymentResponse.value));
const emptyMessagePayMentNews = computed(() =>
isVietnamese
? `Hiện chưa có thanh toán phát sinh giai đoạn ${formatVniDate(dateReport.value["date-start"])} - ${formatVniDate(
dateReport.value["date-end"]
)}`
: `No payments incurred during this period ${formatVniDate(dateReport.value["date-start"])} - ${formatVniDate(
dateReport.value["date-end"]
)}`
);
const paymentStatusSummary = computed(() => {
const payments = Array.isArray(paymentResponse.value) ? paymentResponse.value : [];
const groupedByStatus = groupByField(payments, {
keyField: "status__code",
nameField: "status__name",
nameTransform: (name) => name?.split(". ")?.[1] ?? name,
});
const statusList = groupedByStatus.map((group) => {
const totalAmount = group.children.reduce((sum, payment) => sum + (Number(payment.amount) || 0), 0);
return {
key: group.key ?? "UNKNOWN",
name: group.name ?? "Không xác định",
totalAmount,
};
});
return statusList.reduce(
(acc, item) => {
acc.totalPay += item.totalAmount;
if (item.key === "paid") {
acc.paid += item.totalAmount;
} else {
acc.unpaid += item.totalAmount;
}
return acc;
},
{
totalPay: 0,
paid: 0,
unpaid: 0,
}
);
});
const handlerPaymentSelect = (e, data) => {
e.preventDefault();
e.stopPropagation();
toggleBodyScroll(true);
showPopup.value = true;
dataPaymentSelect.value = data.children ?? [];
labelPaymentType.value = data.name ?? "";
};
const totalAmountPaymentDue = computed(() => {
return paymentDueResponse.value?.reduce((total, mpt) => {
const amount = Number(mpt.amount) || 0;
return total + amount;
}, 0);
});
// <<<<<<<<<<<<<<<<<<<==========================================>>>>>>>>>>>>>>>>>>>>>>>>>>>>
// Handler for popup report
const handlerClosePopup = (e) => {
e.preventDefault();
e.stopPropagation();
// Close popup
showPopup.value = false;
// Clear data
productByStatus.value = null;
dataTransactionSelect.value = null;
handleUnSelectChart();
toggleBodyScroll(false);
};
// <-- End Handler for popup report -->
const handleUnSelectChart = () => {
const chart = Highcharts.charts.find((c) => c);
if (chart) {
chart.series[0]?.points.forEach((p) => {
if (p.selected) p.select(false, false);
});
}
};
// Close popup on ESC key
const onEsc = (e) => {
if (e.key === "Escape") {
handlerClosePopup(e);
handleUnSelectChart();
}
};
onMounted(() => {
window.addEventListener("keydown", onEsc);
});
onBeforeUnmount(() => {
window.removeEventListener("keydown", onEsc);
});
// <-- End Close popup on ESC key -->
watch(
dateReport,
(newVal) => {
foundTransaction.params.filter.create_time__gte = buildDateRange(newVal["date-start"])?.start;
foundTransaction.params.filter.create_time__lte = buildDateRange(newVal["date-end"])?.end;
foundTransactionDue.params.filter.due_date__gte = dateReport.value["date-start"];
foundTransactionDue.params.filter.due_date__lte = dateReport.value["date-end"];
fetchApiTransaction();
foundPayment.params.filter.create_time__gte = buildDateRange(newVal["date-start"])?.start;
foundPayment.params.filter.create_time__lte = buildDateRange(newVal["date-start"])?.end;
foundPaymentDue.params.filter.to_date__gte = dateReport.value["date-start"];
foundPaymentDue.params.filter.to_date__lte = dateReport.value["date-end"];
fetchApiPayment();
console.log("=====> dataTransactionDealer", transactionResponse.value);
},
{ deep: true }
);
</script>
<style>
body.popup-open {
position: fixed;
width: 100%;
overflow: hidden;
}
.report-daily-page .modal-background {
opacity: 0.7 !important;
}
.report-daily-page .modal-card {
width: 90%;
}
.report-daily-page .message-alter {
text-align: center;
font-weight: bolder;
font-style: italic;
}
.report-daily-page .header-report .header-report-title {
font-size: 2rem;
font-weight: bolder;
text-align: center;
text-transform: uppercase;
}
.report-daily-page .header-report .select-date {
padding: 10px;
/* background-color: #f5f5f5; */
width: max-content;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
}
.report-daily-page .header-report .select-date .label-date {
font-weight: bolder;
}
.report-daily-page .report-transaction .card,
.report-daily-page .report-payment .card {
cursor: pointer;
}
/* Container menu */
.report-daily-page .highcharts-contextmenu {
background: #ffffff !important;
border-radius: 8px !important;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12) !important;
padding: 6px 0 !important;
min-width: 180px;
z-index: 9999 !important;
}
/* Item menu */
.report-daily-page .highcharts-contextmenu li {
padding: 10px 16px !important;
font-size: 14px;
color: #333;
cursor: pointer;
transition: all 0.2s ease;
}
/* Hover */
.report-daily-page .highcharts-contextmenu li:hover {
background: #f5f7fa !important;
color: #1e80ff;
}
/* Separator */
.report-daily-page .highcharts-contextmenu hr {
margin: 6px 0 !important;
border-color: #eee !important;
}
.report-daily-page .highcharts-data-table table {
margin: 0 auto;
}
.report-daily-page .highcharts-data-table table th,
.report-daily-page .highcharts-data-table table td {
padding: 10px;
border: 1px solid #ddd;
}
.report-daily-page .highcharts-data-table table thead {
background-color: #204853;
}
.report-daily-page .highcharts-data-table table thead th {
color: #fff !important;
}
.report-daily-page .highcharts-data-table table tbody tr:nth-child(even) {
background-color: #f9f9f9;
}
.report-daily-page .highcharts-data-table table tbody tr:hover {
cursor: pointer;
background-color: #4a4a4a;
}
.report-daily-page .highcharts-data-table table tbody tr:hover,
.report-daily-page .highcharts-data-table table tbody tr:hover th {
color: #fff;
}
</style>