From e551ae7feb9ef1e8eb3821bfe3abba27397b3826 Mon Sep 17 00:00:00 2001 From: efrilm Date: Mon, 6 Oct 2025 17:05:11 +0700 Subject: [PATCH] update report --- .../dashboards/daily-report/page.tsx | 389 ++++++++---------- .../dashboards/daily-report/report-header.tsx | 6 +- 2 files changed, 165 insertions(+), 230 deletions(-) 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 386171d..210223e 100644 --- a/src/app/[lang]/(dashboard)/(private)/dashboards/daily-report/page.tsx +++ b/src/app/[lang]/(dashboard)/(private)/dashboards/daily-report/page.tsx @@ -22,10 +22,9 @@ const DailyPOSReport = () => { startDate: new Date(), endDate: new Date() }) - const [filterType, setFilterType] = useState<'single' | 'range'>('single') // 'single' or 'range' + const [filterType, setFilterType] = useState<'single' | 'range'>('single') const [isGeneratingPDF, setIsGeneratingPDF] = useState(false) - // Use selectedDate for single date filter, or dateRange for range filter const getDateParams = () => { if (filterType === 'single') { return { @@ -66,12 +65,10 @@ const DailyPOSReport = () => { setNow(new Date()) }, []) - // Format date for input field (YYYY-MM-DD) const formatDateForInput = (date: Date) => { return date.toISOString().split('T')[0] } - // Get display text for the report period const getReportPeriodText = () => { if (filterType === 'single') { return `${formatDateDDMMYYYY(selectedDate)} - ${formatDateDDMMYYYY(selectedDate)}` @@ -80,13 +77,10 @@ const DailyPOSReport = () => { } } - // Get report title based on filter type const getReportTitle = () => { if (filterType === 'single') { return 'Laporan Transaksi' } else { - const daysDiff = Math.ceil((dateRange.endDate.getTime() - dateRange.startDate.getTime()) / (1000 * 3600 * 24)) + 1 - // return `Laporan Transaksi ${daysDiff} Hari` return `Laporan Transaksi` } } @@ -99,27 +93,17 @@ const DailyPOSReport = () => { return } - // Set loading state setIsGeneratingPDF(true) try { - // Import jsPDF dan html2canvas const jsPDF = (await import('jspdf')).default const html2canvas = (await import('html2canvas')).default - // Pastikan element terlihat penuh const originalOverflow = reportElement.style.overflow reportElement.style.overflow = 'visible' - // Wait untuk memastikan rendering selesai await new Promise(resolve => setTimeout(resolve, 300)) - console.log('Starting PDF generation...') - - // Update loading message - console.log('Capturing content...') - - // Capture canvas dengan setting yang optimal const canvas = await html2canvas(reportElement, { scale: 1.5, useCORS: true, @@ -132,18 +116,12 @@ const DailyPOSReport = () => { width: reportElement.scrollWidth, scrollX: 0, scrollY: 0, - // Pastikan capture semua content windowWidth: Math.max(reportElement.scrollWidth, window.innerWidth), windowHeight: Math.max(reportElement.scrollHeight, window.innerHeight) }) - console.log('Canvas captured:', canvas.width, 'x', canvas.height) - console.log('Generating PDF pages...') - - // Restore overflow reportElement.style.overflow = originalOverflow - // Create PDF const pdf = new jsPDF({ orientation: 'portrait', unit: 'mm', @@ -151,43 +129,30 @@ const DailyPOSReport = () => { compress: true }) - // A4 dimensions const pdfWidth = 210 const pdfHeight = 297 - const margin = 5 // Small margin to prevent cutoff + const margin = 5 - // Calculate scaling const availableWidth = pdfWidth - 2 * margin const availableHeight = pdfHeight - 2 * margin const imgWidth = availableWidth const imgHeight = (canvas.height * availableWidth) / canvas.width - console.log('PDF dimensions - Canvas:', canvas.width, 'x', canvas.height) - console.log('PDF dimensions - Image:', imgWidth, 'x', imgHeight) - console.log('Available height per page:', availableHeight) - - // Split content across pages let yPosition = margin let sourceY = 0 let pageCount = 1 let remainingHeight = imgHeight while (remainingHeight > 0) { - console.log(`Processing page ${pageCount}, remaining height: ${remainingHeight}`) - if (pageCount > 1) { pdf.addPage() yPosition = margin } - // Calculate how much content fits on this page const heightForThisPage = Math.min(remainingHeight, availableHeight) - - // Calculate source dimensions for cropping const sourceHeight = (heightForThisPage * canvas.height) / imgHeight - // Create temporary canvas for this page portion const tempCanvas = document.createElement('canvas') const tempCtx = tempCanvas.getContext('2d') @@ -198,41 +163,21 @@ const DailyPOSReport = () => { tempCanvas.width = canvas.width tempCanvas.height = sourceHeight - // Draw the portion we need - tempCtx.drawImage( - canvas, - 0, - sourceY, - canvas.width, - sourceHeight, // Source rectangle - 0, - 0, - canvas.width, - sourceHeight // Destination rectangle - ) + tempCtx.drawImage(canvas, 0, sourceY, canvas.width, sourceHeight, 0, 0, canvas.width, sourceHeight) - // Convert to image data const pageImageData = tempCanvas.toDataURL('image/jpeg', 0.9) - - // Add to PDF pdf.addImage(pageImageData, 'JPEG', margin, yPosition, imgWidth, heightForThisPage) - // Update for next page sourceY += sourceHeight remainingHeight -= heightForThisPage pageCount++ - // Safety check to prevent infinite loop if (pageCount > 20) { console.warn('Too many pages, breaking loop') break } } - console.log(`Generated ${pageCount - 1} pages`) - console.log('Finalizing PDF...') - - // Add metadata pdf.setProperties({ title: getReportTitle(), subject: 'Transaction Report', @@ -240,23 +185,16 @@ const DailyPOSReport = () => { creator: 'Apskel' }) - // Generate filename const fileName = filterType === 'single' ? `laporan-transaksi-${formatDateForInput(selectedDate)}.pdf` : `laporan-transaksi-${formatDateForInput(dateRange.startDate)}-to-${formatDateForInput(dateRange.endDate)}.pdf` - console.log('Saving PDF:', fileName) - - // Save PDF pdf.save(fileName) - - console.log('PDF generated successfully!') } catch (error) { console.error('Error generating PDF:', error) alert(`Terjadi kesalahan saat membuat PDF: ${error}`) } finally { - // Reset loading state setIsGeneratingPDF(false) } } @@ -268,15 +206,9 @@ const DailyPOSReport = () => {
- {/* Animated Spinner */}
- - {/* Loading Message */}

{message}

-

Mohon tunggu, proses ini mungkin membutuhkan beberapa detik...

- - {/* Progress Steps */}
@@ -297,12 +229,25 @@ const DailyPOSReport = () => { ) } + // Group products by category + const groupedProducts = + products?.data?.reduce( + (acc, item) => { + const categoryName = item.category_name || 'Tidak Berkategori' + if (!acc[categoryName]) { + acc[categoryName] = [] + } + acc[categoryName].push(item) + return acc + }, + {} as Record + ) || {} + return (
- {/* Control Panel */} + { onSingleDateChange={setSelectedDate} onDateRangeChange={setDateRange} onGeneratePDF={handleGeneratePDF} - // Props opsional - // isGenerating={isGenerating} - // customQuickActions={customQuickActions} - // labels={customLabels} /> - {/* Report Template */}
- {/* Header */} { {/* Performance Summary */}
-

+

Ringkasan

-
-
- Total Penjualan - +
+
+ Total Penjualan + {formatCurrency(profitLoss?.summary.total_revenue ?? 0)}
-
- Total Diskon - +
+ Total Diskon + {formatCurrency(profitLoss?.summary.total_discount ?? 0)}
-
- Total Pajak - +
+ Total Pajak + {formatCurrency(profitLoss?.summary.total_tax ?? 0)}
-
- Total - +
+ Total + {formatCurrency(profitLoss?.summary.total_revenue ?? 0)}
+ {/* Invoice */}
-

+

Invoice

-
-
- Total Invoice - {profitLoss?.summary.total_orders ?? 0} +
+
+ Total Invoice + {profitLoss?.summary.total_orders ?? 0}
{/* Payment Method Summary */}
-

+

Ringkasan Metode Pembayaran

@@ -386,20 +326,20 @@ const DailyPOSReport = () => { - - - - - + + + + + {paymentAnalytics?.data?.map((payment, index) => ( - - + - - + - @@ -420,13 +360,13 @@ const DailyPOSReport = () => { - - - - + + + - +
Metode PembayaranTipeJumlah OrderTotal AmountPersentaseMetode PembayaranTipeJumlah OrderTotal AmountPersentase
{payment.payment_method_name} + {payment.payment_method_name} { {payment.payment_method_type.toUpperCase()} {payment.order_count} + {payment.order_count} {formatCurrency(payment.total_amount)} + {(payment.percentage ?? 0).toFixed(1)}%
TOTAL{paymentAnalytics?.summary.total_orders ?? 0} + TOTAL{paymentAnalytics?.summary.total_orders ?? 0} {formatCurrency(paymentAnalytics?.summary.total_amount ?? 0)}
@@ -435,7 +375,7 @@ const DailyPOSReport = () => { {/* Category Summary */}
-

+

Ringkasan Kategori

@@ -443,17 +383,17 @@ const DailyPOSReport = () => { - - - + + + {category?.data?.map((c, index) => ( - - - + + @@ -461,9 +401,9 @@ const DailyPOSReport = () => { - - - + + @@ -472,134 +412,129 @@ const DailyPOSReport = () => { - {/* Transaction Summary */} + {/* Product Summary - Dipisah per kategori dengan tabel terpisah */}
-

- Ringkasan Item +

+ Ringkasan Item Per Kategori

-
-
-
NamaQtyPendapatanNamaQtyPendapatan
{c.category_name}{c.total_quantity} + {c.category_name}{c.total_quantity} {formatCurrency(c.total_revenue)}
TOTAL{categorySummary?.totalQuantity ?? 0} + TOTAL{categorySummary?.totalQuantity ?? 0} {formatCurrency(categorySummary?.totalRevenue ?? 0)}
- - - - - - - - - - - - - - {(() => { - // Group products by category - const groupedProducts = - products?.data?.reduce( - (acc, item) => { - const categoryName = item.category_name || 'Tidak Berkategori' - if (!acc[categoryName]) { - acc[categoryName] = [] - } - acc[categoryName].push(item) - return acc - }, - {} as Record - ) || {} +
+ {Object.keys(groupedProducts) + .sort() + .map(categoryName => { + const categoryProducts = groupedProducts[categoryName] + const categoryTotalQty = categoryProducts.reduce((sum, item) => sum + (item.quantity_sold || 0), 0) + const categoryTotalRevenue = categoryProducts.reduce((sum, item) => sum + (item.revenue || 0), 0) - const rows: JSX.Element[] = [] - let globalIndex = 0 + return ( +
+ {/* Category Title */} +

+ {categoryName.toUpperCase()} +

- // Sort categories alphabetically - Object.keys(groupedProducts) - .sort() - .forEach(categoryName => { - const categoryProducts = groupedProducts[categoryName] - - // Category header row - rows.push( -
- - - + {/* Category Table */} +
+
ProdukQtyPendapatan
- {categoryName.toUpperCase()} -
+ + + + + + + + + + - ) - - // Product rows for this category - categoryProducts.forEach((item, index) => { - globalIndex++ - rows.push( + + + {categoryProducts.map((item, index) => ( - - - - ) - }) - - // Category subtotal row - const categoryTotalQty = categoryProducts.reduce( - (sum, item) => sum + (item.quantity_sold || 0), - 0 - ) - const categoryTotalRevenue = categoryProducts.reduce( - (sum, item) => sum + (item.revenue || 0), - 0 - ) - - rows.push( - - + + + - - - ) - }) + +
ProdukQtyPendapatan
+ {item.product_name} + {item.quantity_sold} + {formatCurrency(item.revenue)}
+ ))} +
Subtotal {categoryName} + {categoryTotalQty} + {formatCurrency(categoryTotalRevenue)}
+
+
+ ) + })} - return rows - })()} - + {/* Grand Total */} +
+ - - - + + - @@ -611,7 +546,7 @@ const DailyPOSReport = () => { {/* Footer */}
-
+

© 2025 Apskel - Sistem POS Terpadu

Dicetak pada: {now.toLocaleDateString('id-ID')}

diff --git a/src/views/dashboards/daily-report/report-header.tsx b/src/views/dashboards/daily-report/report-header.tsx index 955fa4d..b920096 100644 --- a/src/views/dashboards/daily-report/report-header.tsx +++ b/src/views/dashboards/daily-report/report-header.tsx @@ -38,8 +38,8 @@ const ReportHeader: FC = ({ = ({ {periode && (
TOTAL KESELURUHAN +
+ TOTAL KESELURUHAN + {productSummary.totalQuantitySold ?? 0} + {formatCurrency(productSummary.totalRevenue ?? 0)}