From 16deaf1890edbc9f57aafa05d2b64a955c86c792 Mon Sep 17 00:00:00 2001 From: efrilm Date: Tue, 25 Nov 2025 18:15:18 +0700 Subject: [PATCH] add grafik at report --- package-lock.json | 29 +++ package.json | 2 + .../dashboards/daily-report/page.tsx | 177 ++++++++++++++++-- 3 files changed, 193 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index d421165..d7a1c77 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,6 +47,8 @@ "apexcharts": "3.49.0", "axios": "^1.11.0", "bootstrap-icons": "1.11.3", + "chart.js": "^4.5.1", + "chartjs-plugin-datalabels": "^2.2.0", "classnames": "2.5.1", "cmdk": "1.0.4", "date-fns": "4.1.0", @@ -1830,6 +1832,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@mapbox/jsonlint-lines-primitives": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz", @@ -4696,6 +4704,27 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "node_modules/chartjs-plugin-datalabels": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chartjs-plugin-datalabels/-/chartjs-plugin-datalabels-2.2.0.tgz", + "integrity": "sha512-14ZU30lH7n89oq+A4bWaJPnAG8a7ZTk7dKf48YAzMvJjQtjrgg5Dpk9f+LbjCF6bpx3RAGTeL13IXpKQYyRvlw==", + "license": "MIT", + "peerDependencies": { + "chart.js": ">=3.0.0" + } + }, "node_modules/cheap-ruler": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/cheap-ruler/-/cheap-ruler-4.0.0.tgz", diff --git a/package.json b/package.json index 3b800b2..9e47e91 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,8 @@ "apexcharts": "3.49.0", "axios": "^1.11.0", "bootstrap-icons": "1.11.3", + "chart.js": "^4.5.1", + "chartjs-plugin-datalabels": "^2.2.0", "classnames": "2.5.1", "cmdk": "1.0.4", "date-fns": "4.1.0", diff --git a/src/app/[lang]/(dashboard)/(private)/dashboards/daily-report/page.tsx b/src/app/[lang]/(dashboard)/(private)/dashboards/daily-report/page.tsx index 7f5cbcb..1b9aee1 100644 --- a/src/app/[lang]/(dashboard)/(private)/dashboards/daily-report/page.tsx +++ b/src/app/[lang]/(dashboard)/(private)/dashboards/daily-report/page.tsx @@ -60,8 +60,15 @@ const DailyPOSReport = () => { const dateParams = getDateParams() + const today = new Date() + const sevenDaysAgo = new Date() + sevenDaysAgo.setDate(today.getDate() - 7) + const { data: outlet } = useOutletById() - const { data: sales } = useSalesAnalytics(dateParams) + const { data: profitLoss7Days } = useProfitLossAnalytics({ + date_from: formatDateDDMMYYYY(sevenDaysAgo), + date_to: formatDateDDMMYYYY(today) + }) const { data: profitLoss } = useProfitLossAnalytics(dateParams) const { data: products } = useProductSalesAnalytics(dateParams) const { data: paymentAnalytics } = usePaymentAnalytics(dateParams) @@ -202,6 +209,150 @@ const DailyPOSReport = () => { pdf.addPage() currentY = 20 + // === TREN PENJUALAN (berdasarkan data sales analytics) === + if (profitLoss7Days?.data && profitLoss7Days.data.length > 0) { + pdf.setFontSize(PDF_FONT_SIZES.heading) + pdf.text('Tren Penjualan 7 Hari Terakhir', 14, currentY) + currentY += 10 + + // Create canvas for chart + const canvas = document.createElement('canvas') + canvas.width = 800 + canvas.height = 400 + const ctx = canvas.getContext('2d') + + if (ctx) { + const { Chart, registerables } = await import('chart.js') + Chart.register(...registerables) + + // Prepare chart data + const chartLabels = profitLoss7Days.data.map(day => + new Date(day.date).toLocaleDateString('id-ID', { day: '2-digit', month: 'short' }) + ) + const revenueData = profitLoss7Days.data.map(day => day.revenue) + const ordersData = profitLoss7Days.data.map(day => day.orders) + + // Create chart + new Chart(ctx, { + type: 'bar', + data: { + labels: chartLabels, + datasets: [ + { + label: 'Total Penjualan (Rp)', + data: revenueData, + backgroundColor: 'rgba(54, 23, 94, 0.8)', + borderColor: 'rgba(54, 23, 94, 1)', + borderWidth: 1, + yAxisID: 'y' + }, + { + label: 'Jumlah Invoice', + data: ordersData, + type: 'line', + borderColor: 'rgba(59, 130, 246, 1)', + backgroundColor: 'rgba(59, 130, 246, 0.1)', + borderWidth: 2, + tension: 0.4, + yAxisID: 'y1', + pointRadius: 4, + pointBackgroundColor: 'rgba(59, 130, 246, 1)' + } + ] + }, + options: { + responsive: false, + animation: false, + plugins: { + legend: { + display: true, + position: 'top', + labels: { + font: { size: 12 }, + padding: 15 + } + }, + title: { + display: false + } + }, + scales: { + y: { + type: 'linear', + position: 'left', + beginAtZero: true, + ticks: { + callback: function (value) { + if (typeof value !== 'number') return '' + return 'Rp ' + (value / 1000).toFixed(0) + 'k' + } + }, + + title: { + display: true, + text: 'Total Penjualan' + } + }, + y1: { + type: 'linear', + position: 'right', + beginAtZero: true, + grid: { + drawOnChartArea: false + }, + title: { + display: true, + text: 'Jumlah Invoice' + } + }, + x: { + grid: { + display: false + } + } + } + } + }) + + // Wait for chart to render + await new Promise(resolve => setTimeout(resolve, 500)) + + // Convert chart to image + const chartImage = canvas.toDataURL('image/png', 1.0) + const chartWidth = 180 + const chartHeight = 90 + + pdf.addImage(chartImage, 'PNG', 15, currentY, chartWidth, chartHeight) + currentY += chartHeight + 10 + + // Add summary table below chart + autoTable(pdf, { + startY: currentY, + head: [['Total Invoice', 'Total Penjualan']], + body: [ + [String(profitLoss7Days.summary.total_orders), formatCurrency(profitLoss7Days.summary.total_revenue)] + ], + theme: 'grid', + styles: { + fontSize: PDF_FONT_SIZES.tableContent, + cellPadding: PDF_SPACING.cellPadding, + lineColor: [0, 0, 0], + lineWidth: 0.1, + halign: 'center' + }, + headStyles: { + fillColor: [54, 23, 94], + textColor: 255, + fontStyle: 'bold', + fontSize: PDF_FONT_SIZES.tableHeader + }, + margin: { left: 14, right: 14 } + }) + + currentY = (pdf as any).lastAutoTable.finalY + 20 + } + } + // Payment Method Summary - DENGAN BORDER HITAM pdf.setFontSize(PDF_FONT_SIZES.heading) pdf.text('Ringkasan Metode Pembayaran', 14, currentY) @@ -259,11 +410,11 @@ const DailyPOSReport = () => { didParseCell: data => { if (data.section === 'foot') { if (data.column.index === 0) { - data.cell.styles.halign = 'left' // Kolom pertama: kiri + data.cell.styles.halign = 'left' } else if (data.column.index === 1) { - data.cell.styles.halign = 'center' // Kolom kedua: tengah + data.cell.styles.halign = 'center' } else if (data.column.index === 2) { - data.cell.styles.halign = 'right' // Kolom ketiga: kanan + data.cell.styles.halign = 'right' } } }, @@ -318,11 +469,11 @@ const DailyPOSReport = () => { didParseCell: data => { if (data.section === 'foot') { if (data.column.index === 0) { - data.cell.styles.halign = 'left' // Kolom pertama: kiri + data.cell.styles.halign = 'left' } else if (data.column.index === 1) { - data.cell.styles.halign = 'center' // Kolom kedua: tengah + data.cell.styles.halign = 'center' } else if (data.column.index === 2) { - data.cell.styles.halign = 'right' // Kolom ketiga: kanan + data.cell.styles.halign = 'right' } } }, @@ -362,7 +513,6 @@ const DailyPOSReport = () => { }) .forEach((categoryName, index) => { const categoryProducts = groupedProducts[categoryName].sort((a, b) => { - // Sort by product_sku ASC const skuA = a.product_sku || '' const skuB = b.product_sku || '' return skuA.localeCompare(skuB) @@ -377,21 +527,18 @@ const DailyPOSReport = () => { formatCurrency(item.revenue) ]) - // Check if we need a new page - const estimatedHeight = (productBody.length + 3) * 12 // Adjusted for larger font + const estimatedHeight = (productBody.length + 3) * 12 if (currentY + estimatedHeight > 270) { pdf.addPage() currentY = 20 } - // Category header pdf.setFontSize(PDF_FONT_SIZES.subheading) pdf.setFont('helvetica', 'bold') pdf.setTextColor(54, 23, 94) pdf.text(`${index + 1}. ${categoryName.toUpperCase()}`, 16, currentY) currentY += 15 - // Category table - DENGAN BORDER HITAM autoTable(pdf, { startY: currentY, head: [['Produk', 'Qty', 'Pendapatan']], @@ -429,11 +576,11 @@ const DailyPOSReport = () => { didParseCell: data => { if (data.section === 'foot') { if (data.column.index === 0) { - data.cell.styles.halign = 'left' // Kolom pertama: kiri + data.cell.styles.halign = 'left' } else if (data.column.index === 1) { - data.cell.styles.halign = 'center' // Kolom kedua: tengah + data.cell.styles.halign = 'center' } else if (data.column.index === 2) { - data.cell.styles.halign = 'right' // Kolom ketiga: kanan + data.cell.styles.halign = 'right' } } },