diff --git a/package.json b/package.json index bd2467ca..bdc29566 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@quasar/extras": "^1.16.12", "@tato30/vue-pdf": "^1.11.0", "@vuepic/vue-datepicker": "^8.8.1", + "apexcharts": "^4.5.0", "axios": "^1.7.4", "cropperjs": "^1.6.2", "keycloak-js": "^25.0.4", @@ -34,6 +35,7 @@ "udsv": "^0.6.0", "uuid": "^10.0.0", "vue": "^3.4.38", + "vue3-apexcharts": "^1.7.0", "vue-dragscroll": "^4.0.6", "vue-i18n": "^9.14.0", "vue-pdf": "^4.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4cba44ed..9543f9bc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@vuepic/vue-datepicker': specifier: ^8.8.1 version: 8.8.1(vue@3.4.38(typescript@5.5.4)) + apexcharts: + specifier: ^4.5.0 + version: 4.5.0 axios: specifier: ^1.7.4 version: 1.7.4 @@ -77,6 +80,9 @@ importers: vue-router: specifier: ^4.4.3 version: 4.4.3(vue@3.4.38(typescript@5.5.4)) + vue3-apexcharts: + specifier: ^1.7.0 + version: 1.8.0(apexcharts@4.5.0)(vue@3.4.38(typescript@5.5.4)) devDependencies: '@faker-js/faker': specifier: ^9.3.0 @@ -725,6 +731,31 @@ packages: '@socket.io/component-emitter@3.1.1': resolution: {integrity: sha512-dzJtaDAAoXx4GCOJpbB2eG/Qj8VDpdwkLsWGzGm+0L7E8/434RyMbAHmk9ubXWVAb9nXmc44jUf8GKqVDiKezg==} + '@svgdotjs/svg.draggable.js@3.0.6': + resolution: {integrity: sha512-7iJFm9lL3C40HQcqzEfezK2l+dW2CpoVY3b77KQGqc8GXWa6LhhmX5Ckv7alQfUXBuZbjpICZ+Dvq1czlGx7gA==} + peerDependencies: + '@svgdotjs/svg.js': ^3.2.4 + + '@svgdotjs/svg.filter.js@3.0.9': + resolution: {integrity: sha512-/69XMRCDoam2HgC4ldHIaDgeQf1ViHIsa0Ld4uWgiXtZ+E24DWHe/9Ib6kbNiZ7WRIdlVokUDR1Fg0kjIpkfbw==} + engines: {node: '>= 0.8.0'} + + '@svgdotjs/svg.js@3.2.4': + resolution: {integrity: sha512-BjJ/7vWNowlX3Z8O4ywT58DqbNRyYlkk6Yz/D13aB7hGmfQTvGX4Tkgtm/ApYlu9M7lCQi15xUEidqMUmdMYwg==} + + '@svgdotjs/svg.resize.js@2.0.5': + resolution: {integrity: sha512-4heRW4B1QrJeENfi7326lUPYBCevj78FJs8kfeDxn5st0IYPIRXoTtOSYvTzFWgaWWXd3YCDE6ao4fmv91RthA==} + engines: {node: '>= 14.18'} + peerDependencies: + '@svgdotjs/svg.js': ^3.2.4 + '@svgdotjs/svg.select.js': ^4.0.1 + + '@svgdotjs/svg.select.js@4.0.2': + resolution: {integrity: sha512-5gWdrvoQX3keo03SCmgaBbD+kFftq0F/f2bzCbNnpkkvW6tk4rl4MakORzFuNjvXPWwB4az9GwuvVxQVnjaK2g==} + engines: {node: '>= 14.18'} + peerDependencies: + '@svgdotjs/svg.js': ^3.2.4 + '@tato30/vue-pdf@1.11.0': resolution: {integrity: sha512-GQNfVqq8if6/tezgcW1E0iU7kO5VfzMihdU6sfRnw0ewDYcP43JFt3r2fmDggyAY5lu7ArEwCFvlWi7WbvleCw==} peerDependencies: @@ -979,6 +1010,9 @@ packages: '@xtuc/long@4.2.2': resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + '@yr/monotone-cubic-spline@1.0.3': + resolution: {integrity: sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==} + abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} @@ -1052,6 +1086,9 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} + apexcharts@4.5.0: + resolution: {integrity: sha512-E7ZkrVqPNBUWy/Rmg8DEIqHNBmElzICE/oxOX5Ekvs2ICQUOK/VkEkMH09JGJu+O/EA0NL31hxlmF+wrwrSLaQ==} + aproba@1.2.0: resolution: {integrity: sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==} @@ -3569,6 +3606,12 @@ packages: peerDependencies: vue: ^3.2.0 + vue3-apexcharts@1.8.0: + resolution: {integrity: sha512-5tSD4mXTBbIJ9ir+58qHE6oNtIe0RNgqIRYMKpcsIaxkKtwUww4JhvPkpUFlmiW4OJbbdklgjleXq1lfcM4gdA==} + peerDependencies: + apexcharts: '>=4.0.0' + vue: '>=3.0.0' + vue@3.4.38: resolution: {integrity: sha512-f0ZgN+mZ5KFgVv9wz0f4OgVKukoXtS3nwET4c2vLBGQR50aI8G0cqbFtLlX9Yiyg3LFGBitruPHt2PxwTduJEw==} peerDependencies: @@ -4186,6 +4229,25 @@ snapshots: '@socket.io/component-emitter@3.1.1': {} + '@svgdotjs/svg.draggable.js@3.0.6(@svgdotjs/svg.js@3.2.4)': + dependencies: + '@svgdotjs/svg.js': 3.2.4 + + '@svgdotjs/svg.filter.js@3.0.9': + dependencies: + '@svgdotjs/svg.js': 3.2.4 + + '@svgdotjs/svg.js@3.2.4': {} + + '@svgdotjs/svg.resize.js@2.0.5(@svgdotjs/svg.js@3.2.4)(@svgdotjs/svg.select.js@4.0.2(@svgdotjs/svg.js@3.2.4))': + dependencies: + '@svgdotjs/svg.js': 3.2.4 + '@svgdotjs/svg.select.js': 4.0.2(@svgdotjs/svg.js@3.2.4) + + '@svgdotjs/svg.select.js@4.0.2(@svgdotjs/svg.js@3.2.4)': + dependencies: + '@svgdotjs/svg.js': 3.2.4 + '@tato30/vue-pdf@1.11.0(vue@3.4.38(typescript@5.5.4))': dependencies: pdfjs-dist: 4.5.136 @@ -4549,6 +4611,8 @@ snapshots: '@xtuc/long@4.2.2': {} + '@yr/monotone-cubic-spline@1.0.3': {} + abbrev@1.1.1: optional: true @@ -4625,6 +4689,15 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.1 + apexcharts@4.5.0: + dependencies: + '@svgdotjs/svg.draggable.js': 3.0.6(@svgdotjs/svg.js@3.2.4) + '@svgdotjs/svg.filter.js': 3.0.9 + '@svgdotjs/svg.js': 3.2.4 + '@svgdotjs/svg.resize.js': 2.0.5(@svgdotjs/svg.js@3.2.4)(@svgdotjs/svg.select.js@4.0.2(@svgdotjs/svg.js@3.2.4)) + '@svgdotjs/svg.select.js': 4.0.2(@svgdotjs/svg.js@3.2.4) + '@yr/monotone-cubic-spline': 1.0.3 + aproba@1.2.0: {} aproba@2.0.0: @@ -7513,6 +7586,11 @@ snapshots: '@vue/devtools-api': 6.6.3 vue: 3.4.38(typescript@5.5.4) + vue3-apexcharts@1.8.0(apexcharts@4.5.0)(vue@3.4.38(typescript@5.5.4)): + dependencies: + apexcharts: 4.5.0 + vue: 3.4.38(typescript@5.5.4) + vue@3.4.38(typescript@5.5.4): dependencies: '@vue/compiler-dom': 3.4.38 diff --git a/src/boot/components.ts b/src/boot/components.ts index 0987be70..9983cfed 100644 --- a/src/boot/components.ts +++ b/src/boot/components.ts @@ -4,10 +4,12 @@ import '@vuepic/vue-datepicker/dist/main.css'; import GlobalDialog from 'components/GlobalDialog.vue'; import GlobalLoading from 'components/GlobalLoading.vue'; import VueDragscroll from 'vue-dragscroll'; +import VueApexCharts from 'vue3-apexcharts'; export default boot(({ app }) => { app.component('global-dialog', GlobalDialog); app.component('global-loading', GlobalLoading); app.component('VueDatePicker', VueDatePicker); + app.component('VueApexCharts', VueApexCharts); app.use(VueDragscroll); }); diff --git a/src/components/14_report/Expansion.vue b/src/components/14_report/Expansion.vue new file mode 100644 index 00000000..581c677b --- /dev/null +++ b/src/components/14_report/Expansion.vue @@ -0,0 +1,29 @@ + + + + + diff --git a/src/i18n/eng.ts b/src/i18n/eng.ts index c822240c..51e53bf9 100644 --- a/src/i18n/eng.ts +++ b/src/i18n/eng.ts @@ -1336,4 +1336,41 @@ export default { Succeed: 'Completed', }, }, + + report: { + report: { + title: 'Report', + view: { + Document: 'Document Status Report', + Invoice: 'Payment Report', + Product: 'Product and Service Report', + Sale: 'Sales Summary Report', + }, + document: { + code: 'Code', + status: 'Quotation Status', + createAt: 'Created Date', + updateAt: 'Updated Date', + }, + table: { + code: 'Code', + status: 'Status', + createAt: 'Created Date', + }, + product: { + did: 'Processed Quantity', + sale: 'Sold Quantity', + name: 'Product Name', + code: 'Product Code', + }, + sale: { + byCustomer: 'Sales by Branch', + byProductGroup: 'Sales by Product Category', + bySale: 'Sales by Salesperson', + code: 'Code', + name: 'Name', + count: 'Sold Quantity', + }, + }, + }, }; diff --git a/src/i18n/tha.ts b/src/i18n/tha.ts index c3205cbc..7d7b561f 100644 --- a/src/i18n/tha.ts +++ b/src/i18n/tha.ts @@ -1315,4 +1315,40 @@ export default { Succeed: 'เสร็จสิ้น', }, }, + + report: { + title: 'รายงาน', + view: { + Document: 'รายงานสถานะเอกสาร', + Invoice: 'รายงานการชำระเงิน', + Product: 'รายงานสินค้าและบริการ', + Sale: 'รายงานสรุปยอดขาย', + }, + document: { + code: 'รหัส', + status: 'สถานะใบเสนอราคา', + createAt: 'วันที่สร้าง', + updateAt: 'วันที่แก้ไข', + }, + table: { + code: 'รหัส', + status: 'สถานะ', + createAt: 'วันที่สร้าง', + }, + product: { + did: 'จำนวนที่ออกไปดำเนินการ', + sale: 'จำนวนที่ขายออกไป', + name: 'ชื่อสินค้า', + code: 'รหัสสินค้า', + }, + + sale: { + byCustomer: 'ยอดขายตามสาขา', + byProductGroup: 'ยอดขายตามประเภทสินค้า', + bySale: 'ยอดขายตามพนักงานขาย', + code: 'รหัส', + name: 'ชื่อ', + count: 'จำนวนที่ขาย', + }, + }, }; diff --git a/src/layouts/DrawerComponent.vue b/src/layouts/DrawerComponent.vue index dde0f1ed..5e07ca3a 100644 --- a/src/layouts/DrawerComponent.vue +++ b/src/layouts/DrawerComponent.vue @@ -171,10 +171,9 @@ onMounted(async () => { { label: 'menu.overall', icon: 'mdi-monitor-dashboard', - disabled: true, children: [ - { label: 'report', route: '' }, - { label: 'dashboard', route: '' }, + { label: 'report', route: '/report' }, + { label: 'dashboard', route: '/dash-board' }, ], }, ]; diff --git a/src/pages/14_report/MainPage.vue b/src/pages/14_report/MainPage.vue new file mode 100644 index 00000000..9c72fdee --- /dev/null +++ b/src/pages/14_report/MainPage.vue @@ -0,0 +1,361 @@ + + + + diff --git a/src/pages/14_report/Table/TableReport.vue b/src/pages/14_report/Table/TableReport.vue new file mode 100644 index 00000000..29214126 --- /dev/null +++ b/src/pages/14_report/Table/TableReport.vue @@ -0,0 +1,64 @@ + + diff --git a/src/pages/14_report/constants.ts b/src/pages/14_report/constants.ts new file mode 100644 index 00000000..dd4af0e7 --- /dev/null +++ b/src/pages/14_report/constants.ts @@ -0,0 +1,153 @@ +import { QTableProps } from 'quasar'; +import { Invoice, Receipt } from 'src/stores/payment/types'; +import { + Report, + ReportProduct, + ReportQuotation, +} from 'src/stores/report/types'; +import { formatNumberDecimal } from 'src/stores/utils'; +import { dateFormatJS } from 'src/utils/datetime'; + +export enum ViewMode { + Document = 'document', + Invoice = 'invoice', + Receipt = 'receipt', + Product = 'product', + Sale = 'sale', +} + +type ColumnsSale = { + code: string; + name: string; + _count: number; +}; + +type ColumnsBySale = ColumnsSale & { + url?: string; + id?: string; + gender?: string; +}; + +export const colReportQuotation = [ + { + name: 'code', + align: 'center', + label: 'report.document.code', + field: (data: ReportQuotation) => data.code, + }, + { + name: '#status', + align: 'center', + label: 'report.document.status', + field: '', + }, + { + name: 'createAt', + align: 'center', + label: 'report.document.createAt', + field: (data: ReportQuotation) => dateFormatJS({ date: data.createdAt }), + }, + { + name: 'updateAt', + align: 'center', + label: 'report.document.updateAt', + field: (data: ReportQuotation) => dateFormatJS({ date: data.updatedAt }), + }, +] as const satisfies QTableProps['columns']; + +export const colReport = [ + { + name: 'code', + align: 'center', + label: 'report.table.code', + field: (data: Report) => data.code, + }, + { + name: '#status', + align: 'center', + label: 'report.table.status', + field: '', + }, + { + name: 'createAt', + align: 'center', + label: 'report.table.createAt', + field: (data: Report) => dateFormatJS({ date: data.createdAt }), + }, +] as const satisfies QTableProps['columns']; + +export const colReportProduct = [ + { + name: 'code', + align: 'center', + label: 'report.product.code', + field: (data: ReportProduct) => data.code, + }, + { + name: 'name', + align: 'center', + label: 'report.product.name', + field: (data: ReportProduct) => data.name, + }, + { + name: 'sale', + align: 'center', + label: 'report.product.sale', + field: (data: ReportProduct) => data.sale, + }, + { + name: 'did', + align: 'center', + label: 'report.product.did', + field: (data: ReportProduct) => data.did, + }, +] as const satisfies QTableProps['columns']; + +export const colReportSale = [ + { + name: 'code', + align: 'left', + label: 'report.sale.code', + field: (data: ColumnsSale) => data.code, + }, + { + name: '#name', + align: 'left', + label: 'report.sale.name', + field: '', + }, + { + name: 'count', + align: 'center', + label: 'report.sale.count', + field: (data: ColumnsSale) => data._count, + }, +] as const satisfies QTableProps['columns']; + +export const colReportBySale = [ + { + name: 'code', + align: 'left', + label: 'report.sale.code', + field: (data: ColumnsBySale) => data.code, + }, + { + name: '#name', + align: 'left', + label: 'report.sale.name', + field: '', + }, + { + name: 'count', + align: 'center', + label: 'report.sale.count', + field: (data: ColumnsBySale) => data._count, + }, +] as const satisfies QTableProps['columns']; + +export const pageTabs = [ + { label: 'Document', value: ViewMode.Document, by: ['user'] }, + { label: 'Invoice', value: ViewMode.Invoice, by: ['user'] }, + { label: 'Product', value: ViewMode.Product, by: ['user'] }, + { label: 'Sale', value: ViewMode.Sale, by: ['admin'] }, +]; diff --git a/src/pages/15_dash-board/MainPage.vue b/src/pages/15_dash-board/MainPage.vue new file mode 100644 index 00000000..081ecf2e --- /dev/null +++ b/src/pages/15_dash-board/MainPage.vue @@ -0,0 +1,11 @@ + + + diff --git a/src/router/routes.ts b/src/router/routes.ts index 785f6601..6d752495 100644 --- a/src/router/routes.ts +++ b/src/router/routes.ts @@ -75,16 +75,16 @@ const routes: RouteRecordRaw[] = [ name: 'productAndService', component: () => import('pages/04_product-service/MainPage.vue'), }, - { - path: '/quotation', - name: 'Quotation', - component: () => import('pages/05_quotation/MainPage.vue'), - }, { path: '/workflow', name: 'Workflow', component: () => import('pages/04_flow-managment/MainPage.vue'), }, + { + path: '/quotation', + name: 'Quotation', + component: () => import('pages/05_quotation/MainPage.vue'), + }, { path: '/document-management', name: 'document-management', @@ -105,15 +105,20 @@ const routes: RouteRecordRaw[] = [ name: 'TaskOrder', component: () => import('pages/09_task-order/MainPage.vue'), }, + { + path: '/invoice', + name: '/Invoice', + component: () => import('pages/10_invoice/MainPage.vue'), + }, { path: '/credit-note', name: 'CreditNote', component: () => import('pages/11_credit-note/MainPage.vue'), }, { - path: '/invoice', - name: '/Invoice', - component: () => import('pages/10_invoice/MainPage.vue'), + path: '/debit-note', + name: 'debitNote', + component: () => import('pages/12_debit-note/MainPage.vue'), }, { path: '/receipt', @@ -121,9 +126,14 @@ const routes: RouteRecordRaw[] = [ component: () => import('pages/13_receipt/MainPage.vue'), }, { - path: '/debit-note', - name: 'debitNote', - component: () => import('pages/12_debit-note/MainPage.vue'), + path: '/report', + name: 'report', + component: () => import('pages/14_report/MainPage.vue'), + }, + { + path: '/dash-board', + name: 'dashBoard', + component: () => import('pages/15_dash-board/MainPage.vue'), }, ], }, diff --git a/src/stores/report/index.ts b/src/stores/report/index.ts new file mode 100644 index 00000000..0f0ab14f --- /dev/null +++ b/src/stores/report/index.ts @@ -0,0 +1,71 @@ +import { ref } from 'vue'; +import { defineStore } from 'pinia'; +import { Pagination, Status } from '../types'; +import { api } from 'src/boot/axios'; +import { Report, ReportProduct, ReportQuotation, ReportSale } from './types'; + +const ENDPOINT = 'report'; + +export async function getReportQuotation() { + const res = await api.get(`/${ENDPOINT}/quotation`); + if (res.status < 400) { + res.data; + return res.data; + } + return null; +} + +export async function getReportInvoice() { + const res = await api.get(`/${ENDPOINT}/invoice`); + if (res.status < 400) { + return res.data; + } + return null; +} + +export async function getReportReceipt() { + const res = await api.get(`/${ENDPOINT}/receipt`); + if (res.status < 400) { + return res.data; + } + return null; +} + +export async function getReportSale() { + const res = await api.get(`/${ENDPOINT}/sale`); + + if (res.status < 400) { + return res.data; + } + return null; +} + +export async function getReportProduct() { + const res = await api.get(`/${ENDPOINT}/Product`); + if (res.status < 400) { + return res.data; + } + return null; +} + +export const useReportStore = defineStore('report-store', () => { + const dataReportQuotation = ref([]); + const dataReportInvoice = ref([]); + const dataReportReceipt = ref([]); + const dataReportSale = ref(); + const dataReportProduct = ref([]); + + return { + dataReportQuotation, + dataReportInvoice, + dataReportReceipt, + dataReportSale, + dataReportProduct, + + getReportQuotation, + getReportInvoice, + getReportReceipt, + getReportSale, + getReportProduct, + }; +}); diff --git a/src/stores/report/types.ts b/src/stores/report/types.ts new file mode 100644 index 00000000..92215ae3 --- /dev/null +++ b/src/stores/report/types.ts @@ -0,0 +1,41 @@ +import { QuotationStatus } from 'src/stores/quotations/types'; +import { ProductGroup } from '../product-service/types'; +import { User } from '../user'; +import { CustomerBranch } from '../customer'; + +export type ReportQuotation = { + updatedAt: Date | null; + createdAt: Date | null; + status: QuotationStatus; + code: string; +}; + +export enum Status { + PaymentInProcess = 'PaymentInProcess', + PaymentSuccess = 'PaymentSuccess', + PaymentWait = 'PaymentWait', + PaymentRetry = 'PaymentRetry', +} + +// use with Invoice and Receipt +export type Report = { + createdAt: Date | null; + status: Status; + code: string; +}; + +export type ReportProduct = { + updatedAt: Date | null; + createdAt: Date | null; + order: number; + did: number; + sale: number; + name: string; + code: string; +}; + +export type ReportSale = { + byCustomer: (Omit & { _count: number })[]; + bySale: (User & { _count: number })[]; + byProductGroup: (Omit & { _count: number })[]; +};