'use client' import { useProductSalesAnalytics, useProfitLossAnalytics, useSalesAnalytics, usePaymentAnalytics, useCategoryAnalytics } from '@/services/queries/analytics' import { useOutletById } from '@/services/queries/outlets' import { formatCurrency, formatDate, formatDateDDMMYYYY, formatDatetime } from '@/utils/transform' import ReportGeneratorComponent from '@/views/dashboards/daily-report/report-generator' import ReportHeader from '@/views/dashboards/daily-report/report-header' import React, { useEffect, useRef, useState } from 'react' const DailyPOSReport = () => { const reportRef = useRef(null) const [now, setNow] = useState(new Date()) const [selectedDate, setSelectedDate] = useState(new Date()) const [dateRange, setDateRange] = useState({ startDate: new Date(), endDate: new Date() }) const [filterType, setFilterType] = useState<'single' | 'range'>('single') const [isGeneratingPDF, setIsGeneratingPDF] = useState(false) // PDF Font Size Configuration const PDF_FONT_SIZES = { heading: 20, subheading: 20, tableContent: 14, tableHeader: 14, tableFooter: 14, grandTotal: 18, footer: 11 } // PDF Spacing Configuration const PDF_SPACING = { cellPadding: 5, cellPaddingLarge: 6, sectionGap: 20, headerGap: 15 } const getDateParams = () => { if (filterType === 'single') { return { date_from: formatDateDDMMYYYY(selectedDate), date_to: formatDateDDMMYYYY(selectedDate) } } else { return { date_from: formatDateDDMMYYYY(dateRange.startDate), date_to: formatDateDDMMYYYY(dateRange.endDate) } } } const dateParams = getDateParams() const { data: outlet } = useOutletById() const { data: sales } = useSalesAnalytics(dateParams) const { data: profitLoss } = useProfitLossAnalytics(dateParams) const { data: products } = useProductSalesAnalytics(dateParams) const { data: paymentAnalytics } = usePaymentAnalytics(dateParams) const { data: category } = useCategoryAnalytics(dateParams) const productSummary = { totalQuantitySold: products?.data?.reduce((sum, item) => sum + (item?.quantity_sold || 0), 0) || 0, totalRevenue: products?.data?.reduce((sum, item) => sum + (item?.revenue || 0), 0) || 0, totalOrders: products?.data?.reduce((sum, item) => sum + (item?.order_count || 0), 0) || 0 } const categorySummary = { totalRevenue: category?.data?.reduce((sum, item) => sum + (item?.total_revenue || 0), 0) || 0, orderCount: category?.data?.reduce((sum, item) => sum + (item?.order_count || 0), 0) || 0, productCount: category?.data?.reduce((sum, item) => sum + (item?.product_count || 0), 0) || 0, totalQuantity: category?.data?.reduce((sum, item) => sum + (item?.total_quantity || 0), 0) || 0 } useEffect(() => { setNow(new Date()) }, []) const formatDateForInput = (date: Date) => { return date.toISOString().split('T')[0] } const getReportPeriodText = () => { if (filterType === 'single') { return `${formatDateDDMMYYYY(selectedDate)} - ${formatDateDDMMYYYY(selectedDate)}` } else { return `${formatDateDDMMYYYY(dateRange.startDate)} - ${formatDateDDMMYYYY(dateRange.endDate)}` } } const getReportTitle = () => { if (filterType === 'single') { return 'Laporan Transaksi' } else { return `Laporan Transaksi` } } const handleGeneratePDF = async () => { const reportElement = reportRef.current if (!reportElement) { alert('Report element tidak ditemukan') return } setIsGeneratingPDF(true) try { const jsPDF = (await import('jspdf')).default const html2canvas = (await import('html2canvas')).default const autoTable = (await import('jspdf-autotable')).default const pdf = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4', compress: true }) // Capture header section only (tanpa tabel kategori) const headerElement = reportElement.querySelector('.report-header-section') as HTMLElement if (headerElement) { const headerCanvas = await html2canvas(headerElement, { scale: 2, useCORS: true, backgroundColor: '#ffffff', logging: false, windowWidth: 794 }) const headerImgWidth = 190 const headerImgHeight = (headerCanvas.height * headerImgWidth) / headerCanvas.width const headerImgData = headerCanvas.toDataURL('image/jpeg', 0.95) pdf.addImage(headerImgData, 'JPEG', 10, 10, headerImgWidth, headerImgHeight) } let currentY = 80 // Start position after header // Add summary sections with autoTable pdf.setFontSize(PDF_FONT_SIZES.heading) pdf.setTextColor(54, 23, 94) pdf.setFont('helvetica', 'bold') pdf.text('Ringkasan', 14, currentY) currentY += 15 // Summary table autoTable(pdf, { startY: currentY, head: [], body: [ ['Total Penjualan', formatCurrency(profitLoss?.summary.total_revenue ?? 0)], ['Total Diskon', formatCurrency(profitLoss?.summary.total_discount ?? 0)], ['Total Pajak', formatCurrency(profitLoss?.summary.total_tax ?? 0)], ['Total', formatCurrency(profitLoss?.summary.total_revenue ?? 0)] ], theme: 'plain', styles: { fontSize: PDF_FONT_SIZES.tableContent, cellPadding: PDF_SPACING.cellPadding }, columnStyles: { 0: { fontStyle: 'normal', textColor: [60, 60, 60] }, 1: { halign: 'right', fontStyle: 'bold', textColor: [60, 60, 60] } }, margin: { left: 14, right: 14 } }) currentY = (pdf as any).lastAutoTable.finalY + 20 // Invoice section pdf.setFontSize(PDF_FONT_SIZES.heading) pdf.text('Invoice', 14, currentY) currentY += 15 autoTable(pdf, { startY: currentY, head: [], body: [['Total Invoice', String(profitLoss?.summary.total_orders ?? 0)]], theme: 'plain', styles: { fontSize: PDF_FONT_SIZES.tableContent, cellPadding: PDF_SPACING.cellPadding }, columnStyles: { 0: { fontStyle: 'normal', textColor: [60, 60, 60] }, 1: { halign: 'right', fontStyle: 'bold', textColor: [60, 60, 60] } }, margin: { left: 14, right: 14 } }) pdf.addPage() currentY = 20 // Payment Method Summary pdf.setFontSize(PDF_FONT_SIZES.heading) pdf.text('Ringkasan Metode Pembayaran', 14, currentY) currentY += 15 const paymentBody = paymentAnalytics?.data?.map(payment => [ payment.payment_method_name, payment.payment_method_type.toUpperCase(), String(payment.order_count), formatCurrency(payment.total_amount), `${(payment.percentage ?? 0).toFixed(1)}%` ]) || [] autoTable(pdf, { startY: currentY, head: [['Metode Pembayaran', 'Tipe', 'Jumlah Order', 'Total Amount', 'Persentase']], body: paymentBody, foot: [ [ 'TOTAL', '', String(paymentAnalytics?.summary.total_orders ?? 0), formatCurrency(paymentAnalytics?.summary.total_amount ?? 0), '' ] ], theme: 'striped', styles: { fontSize: PDF_FONT_SIZES.tableContent, cellPadding: PDF_SPACING.cellPadding }, headStyles: { fillColor: [54, 23, 94], textColor: 255, fontStyle: 'bold', fontSize: PDF_FONT_SIZES.tableHeader }, footStyles: { fillColor: [220, 220, 220], textColor: [60, 60, 60], fontStyle: 'bold', fontSize: PDF_FONT_SIZES.tableFooter }, columnStyles: { 1: { halign: 'center' }, 2: { halign: 'center' }, 3: { halign: 'right' }, 4: { halign: 'center' } }, margin: { left: 14, right: 14 } }) currentY = (pdf as any).lastAutoTable.finalY + 20 // Category Summary pdf.setFontSize(PDF_FONT_SIZES.heading) pdf.text('Ringkasan Kategori', 14, currentY) currentY += 15 const categoryBody = category?.data?.map(c => [c.category_name, String(c.total_quantity), formatCurrency(c.total_revenue)]) || [] autoTable(pdf, { startY: currentY, head: [['Nama', 'Qty', 'Pendapatan']], body: categoryBody, foot: [ ['TOTAL', String(categorySummary?.totalQuantity ?? 0), formatCurrency(categorySummary?.totalRevenue ?? 0)] ], theme: 'striped', styles: { fontSize: PDF_FONT_SIZES.tableContent, cellPadding: PDF_SPACING.cellPadding }, headStyles: { fillColor: [54, 23, 94], textColor: 255, fontStyle: 'bold', fontSize: PDF_FONT_SIZES.tableHeader }, footStyles: { fillColor: [220, 220, 220], textColor: [60, 60, 60], fontStyle: 'bold', fontSize: PDF_FONT_SIZES.tableFooter }, columnStyles: { 1: { halign: 'center' }, 2: { halign: 'right' } }, margin: { left: 14, right: 14 } }) // 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 ) || {} // Add new page for product details pdf.addPage() currentY = 20 pdf.setFontSize(PDF_FONT_SIZES.heading) pdf.text('Ringkasan Item Per Kategori', 14, currentY) currentY += 10 // Loop through each category Object.keys(groupedProducts) .sort((a, b) => { const productsA = groupedProducts[a] const productsB = groupedProducts[b] const orderA = productsA[0]?.category_order ?? 999 const orderB = productsB[0]?.category_order ?? 999 return orderA - orderB }) .forEach(categoryName => { 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) }) const categoryTotalQty = categoryProducts.reduce((sum, item) => sum + (item.quantity_sold || 0), 0) const categoryTotalRevenue = categoryProducts.reduce((sum, item) => sum + (item.revenue || 0), 0) const productBody = categoryProducts.map(item => [ item.product_name, String(item.quantity_sold), formatCurrency(item.revenue) ]) // Check if we need a new page const estimatedHeight = (productBody.length + 3) * 12 // Adjusted for larger font if (currentY + estimatedHeight > 270) { pdf.addPage() currentY = 20 } // Category header pdf.setFontSize(20) pdf.setFont('helvetica', 'bold') pdf.setTextColor(54, 23, 94) pdf.text(categoryName.toUpperCase(), 16, currentY) currentY += 15 // Category table autoTable(pdf, { startY: currentY, head: [['Produk', 'Qty', 'Pendapatan']], body: productBody, foot: [[`Subtotal ${categoryName}`, String(categoryTotalQty), formatCurrency(categoryTotalRevenue)]], theme: 'striped', styles: { fontSize: PDF_FONT_SIZES.tableContent, cellPadding: PDF_SPACING.cellPadding }, headStyles: { fillColor: [54, 23, 94], textColor: 255, fontStyle: 'bold', fontSize: PDF_FONT_SIZES.tableHeader }, footStyles: { fillColor: [200, 200, 200], textColor: [60, 60, 60], fontStyle: 'bold', fontSize: 20 }, columnStyles: { 0: { cellWidth: 90 }, 1: { halign: 'center', cellWidth: 40 }, 2: { halign: 'right', cellWidth: 52 } }, margin: { left: 14, right: 14 } }) currentY = (pdf as any).lastAutoTable.finalY + 15 }) // Grand Total if (currentY > 250) { pdf.addPage() currentY = 20 } autoTable(pdf, { startY: currentY, head: [], body: [ ['TOTAL KESELURUHAN', String(productSummary.totalQuantitySold), formatCurrency(productSummary.totalRevenue)] ], theme: 'plain', styles: { fontSize: PDF_FONT_SIZES.grandTotal, cellPadding: 6, fontStyle: 'bold', textColor: [54, 23, 94] }, columnStyles: { 0: { cellWidth: 90 }, 1: { halign: 'center', cellWidth: 40 }, 2: { halign: 'right', cellWidth: 52 } }, margin: { left: 14, right: 14 }, didDrawCell: data => { pdf.setDrawColor(54, 23, 94) pdf.setLineWidth(0.5) } }) // Footer const pageCount = pdf.getNumberOfPages() for (let i = 1; i <= pageCount; i++) { pdf.setPage(i) pdf.setFontSize(11) pdf.setTextColor(120, 120, 120) pdf.text('© 2025 Apskel - Sistem POS Terpadu', 14, 287) pdf.text(`Dicetak pada: ${now.toLocaleDateString('id-ID')}`, 190, 287, { align: 'right' }) } const fileName = filterType === 'single' ? `laporan-transaksi-${formatDateForInput(selectedDate)}.pdf` : `laporan-transaksi-${formatDateForInput(dateRange.startDate)}-to-${formatDateForInput(dateRange.endDate)}.pdf` pdf.save(fileName) } catch (error) { console.error('Error generating PDF:', error) alert(`Terjadi kesalahan saat membuat PDF: ${error}`) } finally { setIsGeneratingPDF(false) } } const LoadingOverlay = ({ isVisible, message = 'Generating PDF...' }: { isVisible: boolean; message?: string }) => { if (!isVisible) return null return (

{message}

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

Capturing report content
Processing pages
Finalizing PDF
) } // 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 (
{/* Wrap header in a section for easy capture */}
{/* Performance Summary */}

Ringkasan

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

Invoice

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

Ringkasan Metode Pembayaran

{paymentAnalytics?.data?.map((payment, index) => ( )) || []}
Metode Pembayaran Tipe Jumlah Order Total Amount Persentase
{payment.payment_method_name} {payment.payment_method_type.toUpperCase()} {payment.order_count} {formatCurrency(payment.total_amount)} {(payment.percentage ?? 0).toFixed(1)}%
TOTAL {paymentAnalytics?.summary.total_orders ?? 0} {formatCurrency(paymentAnalytics?.summary.total_amount ?? 0)}
{/* Category Summary */}

Ringkasan Kategori

{category?.data?.map((c, index) => ( )) || []}
Nama Qty Pendapatan
{c.category_name} {c.total_quantity} {formatCurrency(c.total_revenue)}
TOTAL {categorySummary?.totalQuantity ?? 0} {formatCurrency(categorySummary?.totalRevenue ?? 0)}
{/* Product Summary - Dipisah per kategori dengan tabel terpisah */}

Ringkasan Item Per Kategori

{Object.keys(groupedProducts) .sort((a, b) => { const productsA = groupedProducts[a] const productsB = groupedProducts[b] const orderA = productsA[0]?.category_order ?? 999 const orderB = productsB[0]?.category_order ?? 999 return orderA - orderB }) .map((categoryName, catIndex) => { 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) }) const categoryTotalQty = categoryProducts.reduce((sum, item) => sum + (item.quantity_sold || 0), 0) const categoryTotalRevenue = categoryProducts.reduce((sum, item) => sum + (item.revenue || 0), 0) return (

{categoryName.toUpperCase()}

{categoryProducts.map((item, index) => ( ))}
Produk Qty Pendapatan
{item.product_name} {item.quantity_sold} {formatCurrency(item.revenue)}
Subtotal {categoryName} {categoryTotalQty} {formatCurrency(categoryTotalRevenue)}
) })} {/* Grand Total */}
TOTAL KESELURUHAN {productSummary.totalQuantitySold ?? 0} {formatCurrency(productSummary.totalRevenue ?? 0)}
{/* Footer */}

© 2025 Apskel - Sistem POS Terpadu

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

) } export default DailyPOSReport