1198 lines
36 KiB
Vue
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>
|
|
<Datepicker v-bind="{ record: dateReport, attr: 'date-start' }" @date="select('date-start', $event)" />
|
|
<span class="label-date">đến ngày:</span>
|
|
<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>
|