This commit is contained in:
Viet An
2026-05-04 12:00:07 +07:00
parent e014ceebb2
commit 9bb9ed0809
21 changed files with 357 additions and 2176 deletions

View File

@@ -33,33 +33,35 @@
<template v-for="(v, i) in leftmenu" :key="i" :id="v.code">
<a class="navbar-item rounded-lg is-clipped p-0" v-if="!v.submenu" @click="changeTab(v)">
<span :class="[
'px-3 py-2 fs-14 font-medium',
'px-2 py-2 font-medium',
currentTab.code === v.code ? 'has-text-primary-50 has-background-primary-95' : 'has-text-grey-30'
]">
]"
style="font-size: 13.5px"
>
{{ v[lang] }}
</span>
</a>
<div class="navbar-item has-dropdown is-hoverable" v-else>
<a class="navbar-item px-2" @click="changeTab(v)">
<span :class="[
'px-3 py-1 font-medium',
currentTab.code === v.code ? 'has-text-primary-bold has-background-primary-soft' : 'has-text-grey-30'
]">
<div class="navbar-item rounded-lg has-dropdown is-hoverable" v-else>
<a class="navbar-link rounded-lg p-0" @click="changeTab(v)">
<p
:class="[
'px-2 py-2 rounded-lg font-medium is-flex is-align-items-center',
currentTab.code === v.code ? 'has-text-primary-50 has-background-primary-95' : 'has-text-grey-30'
]"
style="font-size: 13.5px"
>
<span>{{ v[lang] }}</span>
<SvgIcon
style="padding-top: 5px"
v-bind="{ name: 'down2.svg', type: currentTab.code === v.code ? 'white' : 'dark', size: 15 }"
>
</SvgIcon>
</span>
<Icon name="material-symbols:keyboard-arrow-down-rounded" :size="20" />
</p>
</a>
<div class="navbar-dropdown has-background-light">
<div class="navbar-dropdown">
<a
class="navbar-item has-background-light py-1 border-bottom"
v-for="x in v.submenu"
@click="changeTab(v, x)"
v-for="(submenu, i) in v.submenu"
:key="i"
class="navbar-item"
@click="changeTab(v, submenu)"
>
{{ x[lang] }}
{{ submenu[lang] }}
</a>
</div>
</div>
@@ -134,6 +136,186 @@ const menu = [
},
index: 0,
},
{
id: 1,
category: 'topmenu',
classify: 'left',
code: 'rights',
vi: 'Phân quyền',
link: null,
detail: {
base: 'Rights',
component: 'RightsMaster',
},
index: 0,
},
{
id: 1,
category: 'topmenu',
classify: 'left',
code: 'POS',
vi: 'POS',
link: null,
detail: {
base: 'POS',
component: 'POSMaster',
},
index: 0,
},
{
id: 1,
category: 'topmenu',
classify: 'left',
code: 'receipts',
vi: 'Hoá đơn',
link: null,
detail: {
base: 'Receipts',
component: 'ReceiptsMaster',
},
index: 0,
},
{
id: 1,
category: 'topmenu',
classify: 'left',
code: 'imports',
vi: 'Nhập hàng',
link: null,
detail: {
base: 'Imports',
component: 'ImportsMaster',
},
index: 0,
},
{
id: 1,
category: 'topmenu',
classify: 'left',
code: 'exports',
vi: 'Xuất hàng',
link: null,
detail: {
base: 'Exports',
component: 'ExportsMaster',
},
index: 0,
},
{
id: 1,
category: 'topmenu',
classify: 'left',
code: 'inventory-transfer',
vi: 'Chuyển kho',
link: null,
detail: {
base: 'InventoryTransfer',
component: 'InventoryTransferMaster',
},
index: 0,
},
{
id: 1,
category: 'topmenu',
classify: 'left',
code: 'inventory-count',
vi: 'Kiểm kho',
link: null,
detail: {
base: 'InventoryCount',
component: 'InventoryCountMaster',
},
index: 0,
},
{
id: 1,
category: 'topmenu',
classify: 'left',
code: 'cash-book',
vi: 'Sổ quỹ',
link: null,
detail: {
base: 'CashBook',
component: 'CashBookMaster',
},
index: 0,
},
{
id: 1,
category: 'topmenu',
classify: 'left',
code: 'report',
vi: 'Báo cáo',
link: null,
submenu: [
{
id: 1,
category: 'submenu',
classify: 'report',
code: 'ncc',
vi: 'NCC',
link: null,
detail: {
base: 'NCC',
component: 'NCCMaster',
},
index: 0,
},
{
id: 1,
category: 'submenu',
classify: 'report',
code: 'customers',
vi: 'Khách hàng',
link: null,
detail: {
base: 'Customers',
component: 'CustomersMaster',
},
index: 0,
},
{
id: 1,
category: 'submenu',
classify: 'report',
code: 'goods',
vi: 'Hàng hoá',
link: null,
detail: {
base: 'Goods',
component: 'GoodsMaster',
},
index: 0,
},
{
id: 1,
category: 'submenu',
classify: 'report',
code: 'report-cash-book',
vi: 'Sổ quỹ',
link: null,
detail: {
base: 'ReportCashBook',
component: 'ReportCashBookMaster',
},
index: 0,
},
{
id: 1,
category: 'submenu',
classify: 'report',
code: 'finance',
vi: 'Tài chính',
link: null,
detail: {
base: 'Finance',
component: 'FinanceMaster',
},
index: 0,
},
],
index: 0,
},
]
// if($store.rights.length>0) {
// menu = menu.filter(v=>$findIndex($store.rights, {setting: v.id})>=0)
@@ -216,14 +398,35 @@ watch(
.navbar-dropdown {
padding-block: 0.375rem;
overflow: hidden;
}
.navbar-dropdown > .navbar-item {
&:hover {
background-color: hsl(30, 48%, 82%) !important;
}
box-shadow: 0 0.25em 0.5em hsla(var(--bulma-scheme-h), var(--bulma-scheme-s), var(--bulma-scheme-invert-l), 0.1);
> .navbar-item {
&:last-child {
border-bottom: none;
}
}
}
@media screen and (min-width: 768px) {
.navbar-dropdown a.navbar-item {
padding-inline-end: 30px;
}
}
.navbar-item:hover {
.navbar-link {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
> p {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
}
}
.navbar-item > .navbar-link:after {
display: none;
}
</style>

View File

@@ -0,0 +1,7 @@
<script lang="ts" setup>
</script>
<template>
Cash Book
</template>

View File

@@ -11,7 +11,7 @@ const props = defineProps({
@click="$emit('justclick')"
class="rounded-full mx-0 px-0 size-10 font-bold is-flex is-justify-content-center is-align-items-center"
:style="{
border: image ? 'none' : '1px solid #e4e4e4'
border: image ? 'none' : '1px solid var(--bulma-grey-70)'
}"
>
<figure v-if="image" class="image">

View File

@@ -0,0 +1,7 @@
<script lang="ts" setup>
</script>
<template>
Exports
</template>

View File

@@ -0,0 +1,7 @@
<script lang="ts" setup>
</script>
<template>
Import
</template>

View File

@@ -0,0 +1,7 @@
<script lang="ts" setup>
</script>
<template>
Inventory Count
</template>

View File

@@ -0,0 +1,7 @@
<script lang="ts" setup>
</script>
<template>
Inventory Transfer
</template>

View File

@@ -0,0 +1,7 @@
<script lang="ts" setup>
</script>
<template>
POS
</template>

View File

@@ -0,0 +1,7 @@
<script lang="ts" setup>
</script>
<template>
Receipts
</template>

View File

@@ -0,0 +1,7 @@
<script lang="ts" setup>
</script>
<template>
Report Cash Book
</template>

View File

@@ -0,0 +1,7 @@
<script lang="ts" setup>
</script>
<template>
Customers
</template>

View File

@@ -1,800 +0,0 @@
<template>
<div class="report report-daily-page">
<div class="report-header mb-4">
<h2 class="header-report-title">Báo cáo tình hình giao dịch & thanh toán theo ngày</h2>
<div class="select-date">
<span class="label-date">Chọn ngày báo cáo:</span> &nbsp;
<Datepicker v-bind="{ record: infoReport, attr: 'date-report' }" @date="select('date-report', $event)" />
</div>
</div>
<div class="report-body">
<!-- Report Product -->
<div class="report-product">
<div class="fixed-grid has-6-cols-desktop has-3-cols-tablet has-2-cols-mobile">
<div class="grid">
<div class="cell">
<p class="cell-label">Tổng số sản phẩm</p>
<span class="cell-value">{{ totalProduct }}</span>
</div>
<div
class="cell event"
v-for="product in saleStatusProductSummary"
:style="{ color: product.color }"
@click="handlerStatusProduct($event, product)"
>
<p class="cell-label">{{ product.name }}</p>
<span class="cell-value">{{ product.count || 0 }}</span>
</div>
</div>
</div>
<div class="chart-report-product">
<div class="fixed-grid has-1-cols has-3-cols-desktop">
<div class="grid">
<div class="cell">
<div class="chart-status-product">
<client-only>
<highcharts-vue-chart :highcharts="Highcharts" :options="chartProductStatusOptions" />
</client-only>
</div>
</div>
<div class="cell">
<div class="chart-zone-product">
<client-only>
<highcharts-vue-chart :highcharts="Highcharts" :options="chartProductZoneOptions" />
</client-only>
</div>
</div>
<div class="cell">
<div class="chart-product-dealer">
<client-only>
<highcharts-vue-chart :highcharts="Highcharts" :options="chartProductDealerOptions" />
</client-only>
</div>
</div>
</div>
</div>
<div class="chart-cart-product">
<client-only>
<highcharts-vue-chart :highcharts="Highcharts" :options="chartStatusProductDealerOptions" />
</client-only>
</div>
</div>
</div>
<!-- End Report Porduct -->
</div>
<Modal @close="handlerCloseModal" v-bind="showModal" v-if="showModal.lock"> </Modal>
</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];
let selectedChartPie = null;
const productsResponse = ref(null);
const totalProduct = ref(0);
const infoReport = ref({
"date-report": today,
});
const showModal = ref({
title: null,
lock: false,
});
const dataStatusProduct = ref({
totalProduct: 0,
totalValue: 0,
statusCode: null,
statusName: null,
totalLandArea: 0,
totalBuildingArea: 0,
totalFloorArea: 0,
listProduct: [],
});
const listSaleStatus = await $getdata("salestatus");
// const listZoneType = await $getdata("zonetype");
const select = (fieldName, value) => {
infoReport.value[fieldName] = value;
};
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(),
};
}
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()];
};
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 ?? [],
};
});
}
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}`;
};
function formatVND(amount, decimals = 2) {
if (!amount || isNaN(amount)) return "0 đ";
if (amount >= 1_000_000_000) return `${(amount / 1_000_000_000).toFixed(decimals)} tỷ`;
if (amount >= 1_000_000) return `${(amount / 1_000_000).toFixed(decimals)} triệu`;
return `${amount.toLocaleString("vi-VN")} đ`;
}
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";
}
function calcPercentage(part, whole, decimals = 1) {
const numerator = Number(part) || 0;
const denominator = Number(whole) || 0;
if (denominator === 0) return 0;
return Number(((numerator / denominator) * 100).toFixed(decimals));
}
function buildChartSeriesByStatus(statusList = [], parents = [], keyGroup = "status") {
const parentStatusMaps = parents.map(
(parent) => new Map((parent[keyGroup] ?? []).map((st) => [st.key ?? st.code, st.count ?? 0]))
);
return statusList.map((status) => ({
name: status.name?.split(". ")[1] ?? status.name,
color: status.color || undefined,
data: parentStatusMaps.map((map) => ({
y: map.get(status.code) ?? 0,
key: status.code,
})),
}));
}
let scrollPosition = 0;
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);
}
};
// <====== Fetch product list ======>
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,land_lot_size,lot_area,building_area,number_of_floors,cart__code,cart__name,cart__dealer__code,cart__dealer__name,origin_price,txnprd__sale_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 fetchProducts() {
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;
}
}
fetchProducts();
// <====== End fetch product list ======>
// <====== Group status product ======>
const productGroupStatus = 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 productGroupStatusMap = computed(() => buildGroupMap(productGroupStatus.value));
const saleStatusProductSummary = computed(() => {
const merged = mergeMasterWithGroup(listSaleStatus || [], productGroupStatusMap.value);
return merged.map((item) => {
const name = item.name?.split(". ")[1] ?? item.name;
return {
...item,
name,
};
});
});
const handlerStatusProduct = (e, data) => {
e.preventDefault();
e.stopPropagation();
updateProductStatusSummary(data);
showModal.value = {
component: "report/TableReportProduct",
lock: true,
title: `Danh sách sản phẩm - Trạng thái: ${data.name} (${formatVniDate(infoReport["date-date-report"])})`,
width: "80%",
height: "80%",
vbind: { data: dataStatusProduct.value },
};
handlerOpenModal();
};
function updateProductStatusSummary(statusData = {}) {
const listProduct = (statusData.children || []).map((item) => {
const buildingArea = Number(item.building_area ?? 0);
const floors = Number(item.number_of_floors ?? 0);
const totalBuildingArea = buildingArea * floors;
const total_built_area = $numtoString(totalBuildingArea);
item.txnprd__sale_price = item.txnprd__sale_price ?? item.origin_price;
return {
...item,
total_built_area,
};
});
const totals = listProduct.reduce(
(acc, product) => {
const salePrice = Number(product.txnprd__sale_price ?? 0);
const landArea = Number(product.lot_area ?? 0);
const buildingArea = Number(product.building_area ?? 0);
const floors = Number(product.number_of_floors ?? 0);
acc.totalValue += salePrice;
acc.totalLandArea += landArea;
acc.totalBuildingArea += buildingArea;
acc.totalFloorArea += buildingArea * floors;
return acc;
},
{
totalValue: 0,
totalLandArea: 0,
totalBuildingArea: 0,
totalFloorArea: 0,
}
);
dataStatusProduct.value = {
statusCode: statusData.code || statusData.key,
statusName: statusData.name ?? null,
totalProduct: statusData.count ?? 0,
totalValue: totals.totalValue,
totalLandArea: $numtoString(totals.totalLandArea),
totalBuildingArea: $numtoString(totals.totalBuildingArea),
totalFloorArea: $numtoString(totals.totalFloorArea),
listProduct: listProduct,
};
}
// Options chart status product
const chartSelectedStatusProductHandler = () => ({
select() {
selectedChartPie = this;
const item = productGroupStatus.value.find((i) => i.key === this.options.key);
updateProductStatusSummary(item);
showModal.value = {
component: "report/TableReportProduct",
lock: true,
title: `Danh sách sản phẩm - Trạng thái: ${this.options.name} (${formatVniDate(infoReport["date-date-report"])})`,
width: "80%",
height: "80%",
vbind: { data: dataStatusProduct.value },
};
handlerOpenModal();
},
unselect() {
if (selectedChartPie !== this) return;
selectedChartPie = null;
updateProductStatusSummary();
},
});
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: `Dữ liệu ngày: ${formatVniDate(infoReport.value["date-report"])} - ${totalProduct.value} ${
isVietnamese ? "Sản phẩm" : "Products"
}`,
},
plotOptions: {
pie: {
allowPointSelect: true,
cursor: "pointer",
dataLabels: [
{
enabled: true,
distance: 20,
format: "{point.name}: {point.y:,.0f}", // GIÁ TRỊ
},
{
enabled: true,
distance: -40,
format: "({point.percentage:.1f}%)", // GIÁ TRỊ
style: {
fontSize: "0.75em",
textOutline: "none",
opacity: 0.7,
},
filter: {
operator: ">",
property: "percentage",
value: 10,
},
},
],
point: {
events: chartSelectedStatusProductHandler(),
},
},
},
series: [
{
name: isVietnamese ? "Số lượng" : "Quantity",
colorByPoint: true,
data: productGroupStatus.value.map((item) => {
return {
name: item.name,
y: item.count,
key: item.key,
color: item.color,
};
}),
},
],
};
});
// <====== End group status product ======>
// <====== Group zone product ======>
const productGroupZone = computed(() =>
groupByField(productsResponse.value, {
keyField: "zone_type__code",
nameField: "zone_type__name",
})
);
// Options chart zone product
const chartProductZoneOptions = computed(() => {
return {
chart: {
type: "pie",
zooming: {
type: "xy",
},
panning: {
enabled: true,
type: "xy",
},
},
title: {
text: isVietnamese ? "Sản phẩm theo phân khu" : "Products by category",
},
subtitle: {
text: `Dữ liệu ngày: ${formatVniDate(infoReport.value["date-report"])} - ${productGroupZone.value.length} ${
isVietnamese ? "Phân khu" : "Category"
}`,
},
plotOptions: {
pie: {
cursor: "default",
dataLabels: [
{
enabled: true,
distance: 20,
format: "{point.name}: {point.y:,.0f}", // GIÁ TRỊ
},
{
enabled: true,
distance: -40,
format: "({point.percentage:.1f}%)", // GIÁ TRỊ
style: {
fontSize: "0.75em",
textOutline: "none",
opacity: 0.7,
},
filter: {
operator: ">",
property: "percentage",
value: 10,
},
},
],
},
},
series: [
{
name: isVietnamese ? "Số lượng" : "Quantity",
colorByPoint: true,
data: productGroupZone.value.map((item) => {
return {
name: item.name,
y: item.count,
key: item.key,
color: item.color,
};
}),
},
],
};
});
// <====== End group zone product ======>
// <====== Group dealer product ======>
const productGroupDealer = computed(() =>
groupByField(productsResponse.value, {
keyField: "cart__dealer__code",
nameField: "cart__dealer__name",
})
);
// Options chart product dealer
const chartProductDealerOptions = computed(() => {
return {
chart: {
type: "pie",
zooming: {
type: "xy",
},
panning: {
enabled: true,
type: "xy",
},
},
title: {
text: isVietnamese ? "Sản phẩm theo đại lý" : "Products by distributor",
},
subtitle: {
text: `Dữ liệu ngày: ${formatVniDate(infoReport.value["date-report"])} - ${productGroupDealer.value?.length} ${
isVietnamese ? "Đại lý" : "Dealer"
}`,
},
plotOptions: {
pie: {
cursor: "default",
dataLabels: [
{
enabled: true,
distance: 20,
format: "{point.name}: {point.y:,.0f}", // GIÁ TRỊ
},
{
enabled: true,
distance: -40,
format: "({point.percentage:.1f}%)", // GIÁ TRỊ
style: {
fontSize: "0.75em",
textOutline: "none",
opacity: 0.7,
},
filter: {
operator: ">",
property: "percentage",
value: 10,
},
},
],
},
},
series: [
{
name: isVietnamese ? "Số lượng" : "Quantity",
colorByPoint: true,
data:
productGroupDealer.value?.map((item) => {
return {
name: item.key,
y: item.count,
key: item.key,
color: item.color ?? null,
};
}) || [],
},
],
};
});
//Options chart status product dealer
const dealerProductSaleSummary = computed(() =>
(productGroupDealer.value ?? []).map((dealer) => {
const productSaleByStatus = groupByField(dealer.children ?? [], {
keyField: "status__sale_status__code",
nameField: "status__sale_status__name",
nameTransform: (name) => name?.split(". ")[1] ?? name,
colorField: "status__sale_status__color",
});
return {
...dealer,
productSaleByStatus,
};
})
);
const dataChartTransactionPolicy = computed(() => {
const listSaleStatusDealer = listSaleStatus.filter((status) => status.code !== "not-sold");
return buildChartSeriesByStatus(listSaleStatusDealer, dealerProductSaleSummary.value, "productSaleByStatus");
});
const createDealerStatusClickHandler = () => ({
click() {
const dealerCode = this.category?.split(" - ")?.[0] ?? null;
const saleStatusCode = this.options?.key ?? null;
if (!dealerCode || !saleStatusCode) return;
const dealerSummary = dealerProductSaleSummary.value?.find((dealer) => dealer.key === dealerCode);
const productsByStatus = dealerSummary?.productSaleByStatus?.find((status) => status.key === saleStatusCode);
updateProductStatusSummary(productsByStatus);
showModal.value = {
component: "report/TableReportProduct",
lock: true,
title: `Danh sách sản phẩm - Đại lý: ${dealerSummary.key || dealerSummary.code} - Trạng thái: ${
productsByStatus.name
} (${formatVniDate(infoReport["date-date-report"])})`,
width: "80%",
height: "80%",
vbind: { data: dataStatusProduct.value },
};
handlerOpenModal();
},
});
const chartStatusProductDealerOptions = computed(() => {
return {
chart: {
type: "column",
},
title: {
text: isVietnamese ? "Trạng thái sản phẩm theo từng đại lý" : "Product status by dealer",
},
subtitle: {
text: ` Dữ liệu ngày: ${formatVniDate(infoReport.value["date-report"])} `,
},
xAxis: {
categories: dealerProductSaleSummary.value?.map((dealer) => `${dealer.key} - ${dealer.count} SP`),
crosshair: true,
accessibility: {
description: "Countries",
},
},
yAxis: {
min: 0,
title: {
text: "Số lượng",
},
},
tooltip: {
headerFormat: "<b>{category}</b><br/>",
pointFormat: "{series.name}: {point.y} Sản phẩm",
},
plotOptions: {
column: {
cursor: "pointer",
pointPadding: 0.2,
borderWidth: 0,
dataLabels: {
enabled: true,
format: "{point.y}",
},
point: {
events: createDealerStatusClickHandler(),
},
},
},
series: dataChartTransactionPolicy.value,
};
});
// <====== End group dealer product ======>
const handleUnSelectChart = () => {
const chart = Highcharts.charts.find((c) => c);
if (chart) {
chart.series[0]?.points.forEach((p) => {
if (p.selected) p.select(false, false);
});
}
};
const handlerOpenModal = () => {
toggleBodyScroll(true);
};
const handlerCloseModal = () => {
toggleBodyScroll(false);
showModal.value = {
lock: false,
};
handleUnSelectChart();
};
// Reload data change
watch(
infoReport,
(newVal) => {
// foundCustomer.params.filter.create_time__lte = buildDateRange(infoReport.value["date-report"])?.end;
// foundCustomerTransaction.params.filter.txncust__create_time__lte = buildDateRange(
// infoReport.value["date-report"]
// )?.end;
// fetchCustomer();
},
{ deep: true }
);
</script>
<style>
body.popup-open {
position: fixed;
width: 100%;
overflow: hidden;
}
.report-daily-page .report-header {
display: flex;
justify-content: space-between;
}
.report-daily-page .report-header .header-report-title {
font-size: 1.25rem;
font-weight: bolder;
}
.report-daily-page .report-header .select-date {
background-color: #204853;
padding: 10px;
border-radius: 16px;
}
.report-daily-page .report-header .select-date .label-date {
color: #fff;
margin-bottom: 10px;
display: inline-block;
}
.report-daily-page .cell.event {
padding: 10px;
border-radius: 16px;
cursor: pointer;
transition: box-shadow 0.2s ease, transform 0.2s ease;
}
.report-daily-page .cell.event:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.report-daily-page .cell .cell-label {
font-size: 13px;
font-weight: 500;
}
.report-daily-page .cell .cell-value {
font-size: 28px;
font-weight: 700;
line-height: 1.2;
}
.report-daily-page .modal-card {
overflow-y: hidden;
}
@media only screen and (max-width: 767px) {
.report-daily-page .report-header {
flex-direction: column;
gap: 10px;
}
}
</style>

View File

@@ -0,0 +1,7 @@
<script lang="ts" setup>
</script>
<template>
Finance
</template>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
<script lang="ts" setup>
</script>
<template>
Goods
</template>

View File

@@ -1,23 +0,0 @@
<template>
<div class="report report-monthly-page">
<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 tháng</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>
</template>
<script>
</script>
<style>
.report-monthly-page .header-report .header-report-title {
font-size: 2rem;
font-weight: bolder;
text-align: center;
text-transform: uppercase;
}
</style>

View File

@@ -0,0 +1,7 @@
<script lang="ts" setup>
</script>
<template>
NCC
</template>

View File

@@ -1,105 +0,0 @@
<template>
<div class="table-report-product">
<div class="fixed-grid has-2-cols has-5-cols-desktop">
<div class="grid summary-metrics">
<div class="cell metric-item">
<p class="metric-label">Tổng sản phẩm</p>
<span class="metric-value">{{ props.data.totalProduct }}</span>
</div>
<div class="cell metric-item">
<p class="metric-label">Tổng giá trị</p>
<span class="metric-value">{{ formatVND(props.data.totalValue) }}</span>
</div>
<div class="cell metric-item">
<p class="metric-label">Tổng diện tích đất</p>
<span class="metric-value">{{ props.data.totalLandArea }} </span>
</div>
<div class="cell metric-item">
<p class="metric-label">Tổng diện tích xây dựng</p>
<span class="metric-value"> {{ props.data.totalBuildingArea }} </span>
</div>
<div class="cell metric-item">
<p class="metric-label">Tổng diện tích sàn</p>
<span class="metric-value">{{ props.data.totalFloorArea }} </span>
</div>
</div>
</div>
<DataView v-bind="configurationDataView" v-if="configurationDataView && props.data.listProduct.length > 0" />
</div>
</template>
<script setup>
import { ref } from "vue";
const props = defineProps({
data: Object,
});
function formatVND(amount, maxDecimals = 2) {
if (!Number.isFinite(amount)) return "0 đ";
const format = (value) => {
return Number.isInteger(value) ? value.toString() : value.toFixed(maxDecimals).replace(/\.?0+$/, "");
};
if (amount >= 1_000_000_000) return `${format(amount / 1_000_000_000)} tỷ`;
if (amount >= 1_000_000) return `${format(amount / 1_000_000)} triệu`;
return `${amount.toLocaleString("vi-VN")} đ`;
}
const configurationDataView = ref({
data: props.data.listProduct,
setting: "product-list-report",
pagename: "productListReport",
});
</script>
<style>
.table-report-product {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.table-report-product .summary-metrics {
padding: 16px 20px;
background: #fff;
border-radius: 12px;
flex: 0 0 auto;
}
.table-report-product .summary-metrics .metric-item {
display: flex;
flex-direction: column;
gap: 6px;
}
.table-report-product .summary-metrics .metric-label {
font-size: 13px;
color: #6b7280;
font-weight: 500;
}
.table-report-product .summary-metrics .metric-value {
font-size: 20px;
font-weight: 700;
color: #111827;
}
.table-report-product .table-container {
flex: 1 1 auto;
overflow-y: auto;
}
.table-report-product .table-container .table {
position: relative;
}
.table-report-product .table-container .table thead {
position: sticky;
top: 0;
z-index: 10;
}
</style>

View File

@@ -1,8 +1,7 @@
<template>
<div>
Hello
</div>
</template>
<script setup>
</script>
<template>
<div>Rights</div>
</template>

View File

@@ -3,14 +3,14 @@
<TopMenu @changetab="changeTab" />
<div class="container blockdiv has-text-text-20">
<div class="mb-2 is-flex is-justify-content-space-between is-align-items-center is-gap-1" v-if="tab">
<template v-if="subtab">
<span class="fs-17 font-semibold">{{ tab[$store.lang] }}</span>
<SvgIcon class="mx-2" v-bind="{ name: 'right.svg', size: 17, type: 'has-text-black' }"></SvgIcon>
<span class="fs-17 font-semibold">{{ subtab[$store.lang] }}</span>
</template>
<div v-else>
<div class="is-flex is-gap-1 is-align-items-center mb-1">
<p class="fs-17 font-semibold">{{ tab[$store.lang] }}</p>
<div>
<div class="fs-17 font-semibold is-flex is-gap-1 is-align-items-center mb-1">
<div v-if="subtab" class="is-flex is-gap-0.5 is-align-items-center">
<span>{{ tab[$store.lang] }}</span>
<Icon name="material-symbols:arrow-forward-ios-rounded" :size="16" />
<span>{{ subtab[$store.lang] }}</span>
</div>
<p v-else>{{ tab[$store.lang] }}</p>
<button
@click="refresh"
class="button is-primary is-light rounded-full p-1"

View File

@@ -2,6 +2,19 @@ import { defineNuxtPlugin } from "#app";
import Dashboard from '@/components/dashboard/Dashboard.vue';
import Orders from '@/components/orders/Orders.vue';
import Inventory from '@/components/inventory/Inventory.vue';
import Rights from '@/components/rights/Rights.vue';
import POS from '@/components/pos/POS.vue';
import Receipts from '@/components/receipts/Receipts.vue';
import Imports from '@/components/imports/Imports.vue';
import Exports from '@/components/exports/Exports.vue';
import InventoryTransfer from '@/components/inventory-transfer/InventoryTransfer.vue';
import InventoryCount from '@/components/inventory-count/InventoryCount.vue';
import CashBook from '@/components/cash-book/CashBook.vue';
import NCC from '@/components/report/NCC.vue';
import Customers from '@/components/report/Customers.vue';
import Goods from '@/components/report/Goods.vue';
import ReportCashBook from '@/components/report/CashBook.vue';
import Finance from '@/components/report/Finance.vue';
import Notebox from "~/components/common/Notebox.vue";
import ProductCountbox from "~/components/common/ProductCountbox.vue";
import SvgIcon from "~/components/SvgIcon.vue";
@@ -21,9 +34,6 @@ import InternalEntry from "~/components/modal/InternalEntry.vue"
import Configuration from "~/components/maintab/Configuration.vue";
import DebtView from "~/components/accounting/DebtView.vue";
import ReportDaily from "~/components/report/Daily.vue";
import ReportFormTo from "~/components/report/FromTo.vue";
import ReportMonthly from "~/components/report/Monthly.vue";
//format
import FormatNumber from "~/components/datatable/format/FormatNumber.vue";
@@ -106,9 +116,6 @@ const components = {
MenuParam,
ScrollBox,
MenuPayment,
ReportDaily,
ReportFormTo,
ReportMonthly,
DataModel,
FormatNumber,
MenuApp,
@@ -131,7 +138,20 @@ const components = {
Overdue,
Dashboard,
Orders,
Inventory
Inventory,
Rights,
POS,
Receipts,
Imports,
Exports,
InventoryTransfer,
InventoryCount,
CashBook,
NCC,
Customers,
Goods,
ReportCashBook,
Finance,
};
export default defineNuxtPlugin((nuxtApp) => {