fix: report page

This commit is contained in:
efrilm 2025-09-23 15:30:42 +07:00
parent 726d1f4984
commit 256f035997
2 changed files with 173 additions and 41 deletions

View File

@ -23,6 +23,7 @@ const DailyPOSReport = () => {
endDate: new Date() endDate: new Date()
}) })
const [filterType, setFilterType] = useState<'single' | 'range'>('single') // 'single' or 'range' const [filterType, setFilterType] = useState<'single' | 'range'>('single') // 'single' or 'range'
const [isGeneratingPDF, setIsGeneratingPDF] = useState(false)
// Use selectedDate for single date filter, or dateRange for range filter // Use selectedDate for single date filter, or dateRange for range filter
const getDateParams = () => { const getDateParams = () => {
@ -101,81 +102,212 @@ const DailyPOSReport = () => {
const handleGeneratePDF = async () => { const handleGeneratePDF = async () => {
const reportElement = reportRef.current const reportElement = reportRef.current
if (!reportElement) {
alert('Report element tidak ditemukan')
return
}
// Set loading state
setIsGeneratingPDF(true)
try { try {
// Import jsPDF dan html2canvas // Import jsPDF dan html2canvas
const jsPDF = (await import('jspdf')).default const jsPDF = (await import('jspdf')).default
const html2canvas = (await import('html2canvas')).default const html2canvas = (await import('html2canvas')).default
// Optimized canvas capture dengan scale lebih rendah // Pastikan element terlihat penuh
const canvas = await html2canvas(reportElement!, { const originalOverflow = reportElement.style.overflow
scale: 1.5, // Reduced from 2 to 1.5 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, useCORS: true,
allowTaint: true, allowTaint: true,
backgroundColor: '#ffffff', backgroundColor: '#ffffff',
logging: false, // Disable logging for performance logging: false,
removeContainer: true, // Clean up after capture removeContainer: true,
imageTimeout: 0, // No timeout for image loading imageTimeout: 0,
height: reportElement!.scrollHeight, height: reportElement.scrollHeight,
width: reportElement!.scrollWidth 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)
}) })
// Compress canvas using JPEG with quality setting console.log('Canvas captured:', canvas.width, 'x', canvas.height)
const imgData = canvas.toDataURL('image/jpeg', 0.85) // JPEG with 85% quality instead of PNG console.log('Generating PDF pages...')
// Create PDF with compression // Restore overflow
reportElement.style.overflow = originalOverflow
// Create PDF
const pdf = new jsPDF({ const pdf = new jsPDF({
orientation: 'portrait', orientation: 'portrait',
unit: 'mm', unit: 'mm',
format: 'a4', format: 'a4',
compress: true // Enable built-in compression compress: true
}) })
const imgWidth = 210 // A4 dimensions
const pageHeight = 295 const pdfWidth = 210
const imgHeight = (canvas.height * imgWidth) / canvas.width const pdfHeight = 297
let heightLeft = imgHeight const margin = 5 // Small margin to prevent cutoff
let position = 0
// Add first page with compressed image // Calculate scaling
pdf.addImage(imgData, 'JPEG', 0, position, imgWidth, imgHeight, '', 'FAST') // Use FAST compression const availableWidth = pdfWidth - 2 * margin
heightLeft -= pageHeight const availableHeight = pdfHeight - 2 * margin
// Handle multiple pages if needed const imgWidth = availableWidth
while (heightLeft >= 0) { const imgHeight = (canvas.height * availableWidth) / canvas.width
position = heightLeft - imgHeight
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() pdf.addPage()
pdf.addImage(imgData, 'JPEG', 0, position, imgWidth, imgHeight, '', 'FAST') yPosition = margin
heightLeft -= pageHeight
} }
// Additional compression options // 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')
if (!tempCtx) {
throw new Error('Unable to get 2D context from canvas')
}
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
)
// 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({ pdf.setProperties({
title: `${getReportTitle()}`, title: getReportTitle(),
subject: 'Daily Transaction Report', subject: 'Transaction Report',
author: 'Apskel POS System', author: 'Apskel POS System',
creator: 'Apskel' creator: 'Apskel'
}) })
// Save with optimized settings // Generate filename
const fileName = const fileName =
filterType === 'single' filterType === 'single'
? `laporan-transaksi-${formatDateForInput(selectedDate)}.pdf` ? `laporan-transaksi-${formatDateForInput(selectedDate)}.pdf`
: `laporan-transaksi-${formatDateForInput(dateRange.startDate)}-to-${formatDateForInput(dateRange.endDate)}.pdf` : `laporan-transaksi-${formatDateForInput(dateRange.startDate)}-to-${formatDateForInput(dateRange.endDate)}.pdf`
pdf.save(fileName, { console.log('Saving PDF:', fileName)
returnPromise: true
})
// Clean up canvas to free memory // Save PDF
canvas.width = canvas.height = 0 pdf.save(fileName)
console.log('PDF generated successfully!')
} catch (error) { } catch (error) {
console.error('Error generating PDF:', error) console.error('Error generating PDF:', error)
alert('Terjadi kesalahan saat membuat PDF. Pastikan jsPDF dan html2canvas sudah terinstall.') alert(`Terjadi kesalahan saat membuat PDF: ${error}`)
} finally {
// Reset loading state
setIsGeneratingPDF(false)
} }
} }
const LoadingOverlay = ({ isVisible, message = 'Generating PDF...' }: { isVisible: boolean; message?: string }) => {
if (!isVisible) return null
return (
<div className='fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 backdrop-blur-sm'>
<div className='bg-white rounded-lg shadow-xl p-8 max-w-sm w-full mx-4'>
<div className='text-center'>
{/* Animated Spinner */}
<div className='inline-block animate-spin rounded-full h-12 w-12 border-4 border-gray-200 border-t-purple-600 mb-4'></div>
{/* Loading Message */}
<h3 className='text-lg font-semibold text-gray-800 mb-2'>{message}</h3>
<p className='text-sm text-gray-600'>Mohon tunggu, proses ini mungkin membutuhkan beberapa detik...</p>
{/* Progress Steps */}
<div className='mt-6 space-y-2'>
<div className='flex items-center text-xs text-gray-500'>
<div className='w-2 h-2 bg-green-500 rounded-full mr-2 flex-shrink-0'></div>
<span>Capturing report content</span>
</div>
<div className='flex items-center text-xs text-gray-500'>
<div className='w-2 h-2 bg-blue-500 rounded-full mr-2 flex-shrink-0 animate-pulse'></div>
<span>Processing pages</span>
</div>
<div className='flex items-center text-xs text-gray-500'>
<div className='w-2 h-2 bg-gray-300 rounded-full mr-2 flex-shrink-0'></div>
<span>Finalizing PDF</span>
</div>
</div>
</div>
</div>
</div>
)
}
return ( return (
<div className='min-h-screen'> <div className='min-h-screen'>
<LoadingOverlay isVisible={isGeneratingPDF} message='Membuat PDF Laporan...' />
{/* Control Panel */} {/* Control Panel */}
<ReportGeneratorComponent <ReportGeneratorComponent
// Props wajib // Props wajib

View File

@ -61,7 +61,7 @@ const ReportHeader: FC<ReportHeaderProps> = ({
sx={{ sx={{
fontWeight: 600, fontWeight: 600,
mt: 1, mt: 1,
color: theme.palette.text.primary color: '#222222'
}} }}
> >
{outlet.name} {outlet.name}
@ -72,7 +72,7 @@ const ReportHeader: FC<ReportHeaderProps> = ({
variant='body2' variant='body2'
sx={{ sx={{
mt: 1, mt: 1,
color: theme.palette.text.secondary color: '#222222'
}} }}
> >
{outlet.address} {outlet.address}
@ -88,21 +88,21 @@ const ReportHeader: FC<ReportHeaderProps> = ({
component='h3' component='h3'
sx={{ sx={{
fontWeight: 700, fontWeight: 700,
color: theme.palette.text.primary color: '#222222'
}} }}
> >
{reportTitle} {reportTitle}
</Typography> </Typography>
{periode && ( {periode && (
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 2, mt: 2 }}> <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 2, mt: 2 }}>
<Typography variant='body2' sx={{ color: theme.palette.text.secondary }}> <Typography variant='body2' sx={{ color: '#222222' }}>
{periode} {periode}
</Typography> </Typography>
</Box> </Box>
)} )}
{reportSubtitle && ( {reportSubtitle && (
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 2, mt: 2 }}> <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 2, mt: 2 }}>
<Typography variant='body2' sx={{ color: theme.palette.text.secondary }}> <Typography variant='body2' sx={{ color: '#222222' }}>
{reportSubtitle} {reportSubtitle}
</Typography> </Typography>
</Box> </Box>