add grafik at report
This commit is contained in:
parent
f434373eaf
commit
16deaf1890
29
package-lock.json
generated
29
package-lock.json
generated
@ -47,6 +47,8 @@
|
|||||||
"apexcharts": "3.49.0",
|
"apexcharts": "3.49.0",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"bootstrap-icons": "1.11.3",
|
"bootstrap-icons": "1.11.3",
|
||||||
|
"chart.js": "^4.5.1",
|
||||||
|
"chartjs-plugin-datalabels": "^2.2.0",
|
||||||
"classnames": "2.5.1",
|
"classnames": "2.5.1",
|
||||||
"cmdk": "1.0.4",
|
"cmdk": "1.0.4",
|
||||||
"date-fns": "4.1.0",
|
"date-fns": "4.1.0",
|
||||||
@ -1830,6 +1832,12 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@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": {
|
"node_modules/@mapbox/jsonlint-lines-primitives": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz",
|
"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"
|
"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": {
|
"node_modules/cheap-ruler": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/cheap-ruler/-/cheap-ruler-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/cheap-ruler/-/cheap-ruler-4.0.0.tgz",
|
||||||
|
|||||||
@ -53,6 +53,8 @@
|
|||||||
"apexcharts": "3.49.0",
|
"apexcharts": "3.49.0",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"bootstrap-icons": "1.11.3",
|
"bootstrap-icons": "1.11.3",
|
||||||
|
"chart.js": "^4.5.1",
|
||||||
|
"chartjs-plugin-datalabels": "^2.2.0",
|
||||||
"classnames": "2.5.1",
|
"classnames": "2.5.1",
|
||||||
"cmdk": "1.0.4",
|
"cmdk": "1.0.4",
|
||||||
"date-fns": "4.1.0",
|
"date-fns": "4.1.0",
|
||||||
|
|||||||
@ -60,8 +60,15 @@ const DailyPOSReport = () => {
|
|||||||
|
|
||||||
const dateParams = getDateParams()
|
const dateParams = getDateParams()
|
||||||
|
|
||||||
|
const today = new Date()
|
||||||
|
const sevenDaysAgo = new Date()
|
||||||
|
sevenDaysAgo.setDate(today.getDate() - 7)
|
||||||
|
|
||||||
const { data: outlet } = useOutletById()
|
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: profitLoss } = useProfitLossAnalytics(dateParams)
|
||||||
const { data: products } = useProductSalesAnalytics(dateParams)
|
const { data: products } = useProductSalesAnalytics(dateParams)
|
||||||
const { data: paymentAnalytics } = usePaymentAnalytics(dateParams)
|
const { data: paymentAnalytics } = usePaymentAnalytics(dateParams)
|
||||||
@ -202,6 +209,150 @@ const DailyPOSReport = () => {
|
|||||||
pdf.addPage()
|
pdf.addPage()
|
||||||
currentY = 20
|
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
|
// Payment Method Summary - DENGAN BORDER HITAM
|
||||||
pdf.setFontSize(PDF_FONT_SIZES.heading)
|
pdf.setFontSize(PDF_FONT_SIZES.heading)
|
||||||
pdf.text('Ringkasan Metode Pembayaran', 14, currentY)
|
pdf.text('Ringkasan Metode Pembayaran', 14, currentY)
|
||||||
@ -259,11 +410,11 @@ const DailyPOSReport = () => {
|
|||||||
didParseCell: data => {
|
didParseCell: data => {
|
||||||
if (data.section === 'foot') {
|
if (data.section === 'foot') {
|
||||||
if (data.column.index === 0) {
|
if (data.column.index === 0) {
|
||||||
data.cell.styles.halign = 'left' // Kolom pertama: kiri
|
data.cell.styles.halign = 'left'
|
||||||
} else if (data.column.index === 1) {
|
} 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) {
|
} 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 => {
|
didParseCell: data => {
|
||||||
if (data.section === 'foot') {
|
if (data.section === 'foot') {
|
||||||
if (data.column.index === 0) {
|
if (data.column.index === 0) {
|
||||||
data.cell.styles.halign = 'left' // Kolom pertama: kiri
|
data.cell.styles.halign = 'left'
|
||||||
} else if (data.column.index === 1) {
|
} 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) {
|
} 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) => {
|
.forEach((categoryName, index) => {
|
||||||
const categoryProducts = groupedProducts[categoryName].sort((a, b) => {
|
const categoryProducts = groupedProducts[categoryName].sort((a, b) => {
|
||||||
// Sort by product_sku ASC
|
|
||||||
const skuA = a.product_sku || ''
|
const skuA = a.product_sku || ''
|
||||||
const skuB = b.product_sku || ''
|
const skuB = b.product_sku || ''
|
||||||
return skuA.localeCompare(skuB)
|
return skuA.localeCompare(skuB)
|
||||||
@ -377,21 +527,18 @@ const DailyPOSReport = () => {
|
|||||||
formatCurrency(item.revenue)
|
formatCurrency(item.revenue)
|
||||||
])
|
])
|
||||||
|
|
||||||
// Check if we need a new page
|
const estimatedHeight = (productBody.length + 3) * 12
|
||||||
const estimatedHeight = (productBody.length + 3) * 12 // Adjusted for larger font
|
|
||||||
if (currentY + estimatedHeight > 270) {
|
if (currentY + estimatedHeight > 270) {
|
||||||
pdf.addPage()
|
pdf.addPage()
|
||||||
currentY = 20
|
currentY = 20
|
||||||
}
|
}
|
||||||
|
|
||||||
// Category header
|
|
||||||
pdf.setFontSize(PDF_FONT_SIZES.subheading)
|
pdf.setFontSize(PDF_FONT_SIZES.subheading)
|
||||||
pdf.setFont('helvetica', 'bold')
|
pdf.setFont('helvetica', 'bold')
|
||||||
pdf.setTextColor(54, 23, 94)
|
pdf.setTextColor(54, 23, 94)
|
||||||
pdf.text(`${index + 1}. ${categoryName.toUpperCase()}`, 16, currentY)
|
pdf.text(`${index + 1}. ${categoryName.toUpperCase()}`, 16, currentY)
|
||||||
currentY += 15
|
currentY += 15
|
||||||
|
|
||||||
// Category table - DENGAN BORDER HITAM
|
|
||||||
autoTable(pdf, {
|
autoTable(pdf, {
|
||||||
startY: currentY,
|
startY: currentY,
|
||||||
head: [['Produk', 'Qty', 'Pendapatan']],
|
head: [['Produk', 'Qty', 'Pendapatan']],
|
||||||
@ -429,11 +576,11 @@ const DailyPOSReport = () => {
|
|||||||
didParseCell: data => {
|
didParseCell: data => {
|
||||||
if (data.section === 'foot') {
|
if (data.section === 'foot') {
|
||||||
if (data.column.index === 0) {
|
if (data.column.index === 0) {
|
||||||
data.cell.styles.halign = 'left' // Kolom pertama: kiri
|
data.cell.styles.halign = 'left'
|
||||||
} else if (data.column.index === 1) {
|
} 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) {
|
} else if (data.column.index === 2) {
|
||||||
data.cell.styles.halign = 'right' // Kolom ketiga: kanan
|
data.cell.styles.halign = 'right'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user