diff --git a/src/services/export/pdf/PDFExportPaymentService.ts b/src/services/export/pdf/PDFExportPaymentService.ts new file mode 100644 index 0000000..dd19deb --- /dev/null +++ b/src/services/export/pdf/PDFExportPaymentService.ts @@ -0,0 +1,364 @@ +// services/pdfExportPaymentService.ts +import { PaymentReport } from '@/types/services/analytic' + +export class PDFExportPaymentService { + /** + * Export Payment Method Report to PDF + */ + static async exportPaymentMethodToPDF(paymentData: PaymentReport, filename?: string) { + try { + // Dynamic import untuk jsPDF + const jsPDFModule = await import('jspdf') + const jsPDF = jsPDFModule.default + + // Create new PDF document - PORTRAIT A4 + const pdf = new jsPDF('p', 'mm', 'a4') + + // Add content + await this.addPaymentReportContent(pdf, paymentData) + + // Generate filename + const exportFilename = filename || this.generateFilename('Laporan_Metode_Pembayaran', 'pdf') + + // Save PDF + pdf.save(exportFilename) + + return { success: true, filename: exportFilename } + } catch (error) { + console.error('Error exporting payment report to PDF:', error) + return { success: false, error: `PDF export failed: ${(error as Error).message}` } + } + } + + /** + * Add payment report content to PDF + */ + private static async addPaymentReportContent(pdf: any, paymentData: PaymentReport) { + let yPos = 20 + const pageWidth = pdf.internal.pageSize.getWidth() + const pageHeight = pdf.internal.pageSize.getHeight() + const marginLeft = 20 + const marginRight = 20 + const marginBottom = 15 + + // Helper function to check page break + const checkPageBreak = (neededSpace: number) => { + if (yPos + neededSpace > pageHeight - marginBottom) { + pdf.addPage() + yPos = 20 + return true + } + return false + } + + // Title + yPos = this.addReportTitle(pdf, paymentData, yPos, pageWidth, marginLeft, marginRight) + + // Section 1: Ringkasan + checkPageBreak(50) + yPos = this.addRingkasanSection(pdf, paymentData, yPos, pageWidth, marginLeft, marginRight, checkPageBreak) + + // Section 2: Payment Methods Detail + checkPageBreak(80) + yPos = this.addPaymentMethodsSection(pdf, paymentData, yPos, pageWidth, marginLeft, marginRight, checkPageBreak) + } + + /** + * Add report title + */ + private static addReportTitle( + pdf: any, + paymentData: PaymentReport, + startY: number, + pageWidth: number, + marginLeft: number, + marginRight: number + ): number { + let yPos = startY + + // Title + pdf.setFontSize(20) + pdf.setFont('helvetica', 'bold') + pdf.setTextColor(0, 0, 0) + pdf.text('Laporan Metode Pembayaran', pageWidth / 2, yPos, { align: 'center' }) + yPos += 10 + + // Period + pdf.setFontSize(12) + pdf.setFont('helvetica', 'normal') + const periodText = `${paymentData.date_from.split('T')[0]} - ${paymentData.date_to.split('T')[0]}` + pdf.text(periodText, pageWidth / 2, yPos, { align: 'center' }) + yPos += 10 + + // Purple line separator + pdf.setDrawColor(102, 45, 145) + pdf.setLineWidth(2) + pdf.line(marginLeft, yPos, pageWidth - marginRight, yPos) + yPos += 15 + + return yPos + } + + /** + * Add Ringkasan section - SAMA SEPERTI SALES REPORT STYLE + */ + private static addRingkasanSection( + pdf: any, + paymentData: PaymentReport, + startY: number, + pageWidth: number, + marginLeft: number, + marginRight: number, + checkPageBreak: (space: number) => boolean + ): number { + let yPos = startY + + // Section title - SAMA SEPERTI SALES REPORT + pdf.setFontSize(14) + pdf.setFont('helvetica', 'bold') + pdf.setTextColor(102, 45, 145) + pdf.text('Ringkasan', marginLeft, yPos) + yPos += 12 + + // Reset text color + pdf.setTextColor(0, 0, 0) + pdf.setFontSize(11) + + const ringkasanItems = [ + { label: 'Total Amount', value: this.formatCurrency(paymentData.summary.total_amount), bold: false }, + { label: 'Total Orders', value: paymentData.summary.total_orders.toString(), bold: false }, + { label: 'Total Payments', value: paymentData.summary.total_payments.toString(), bold: false }, + { label: 'Average Order Value', value: this.formatCurrency(paymentData.summary.average_order_value), bold: true } + ] + + ringkasanItems.forEach((item, index) => { + if (checkPageBreak(15)) yPos = 20 + + // Set font weight + pdf.setFont('helvetica', item.bold ? 'bold' : 'normal') + + pdf.text(item.label, marginLeft, yPos) + pdf.text(item.value, pageWidth - marginRight, yPos, { align: 'right' }) + + // Light separator line (except for last row) + if (!item.bold) { + pdf.setDrawColor(230, 230, 230) + pdf.setLineWidth(0.5) + pdf.line(marginLeft, yPos + 2, pageWidth - marginRight, yPos + 2) + } + + yPos += 8 + }) + + return yPos + 20 + } + + /** + * Add Payment Methods section - ORIGINAL STYLE DARI SALES REPORT + */ + private static addPaymentMethodsSection( + pdf: any, + paymentData: PaymentReport, + startY: number, + pageWidth: number, + marginLeft: number, + marginRight: number, + checkPageBreak: (space: number) => boolean + ): number { + let yPos = startY + + // Section title + pdf.setFontSize(14) + pdf.setFont('helvetica', 'bold') + pdf.setTextColor(102, 45, 145) + pdf.text('Rincian Metode Pembayaran', marginLeft, yPos) + yPos += 12 + + // Reset formatting + pdf.setTextColor(0, 0, 0) + + // Table setup + const tableWidth = pageWidth - marginLeft - marginRight + const colWidths = [50, 25, 30, 35, 25] // Method, Type, Order, Amount, % + let currentX = marginLeft + + // Table header + pdf.setFillColor(240, 240, 240) + pdf.rect(marginLeft, yPos, tableWidth, 10, 'F') + + pdf.setFont('helvetica', 'bold') + pdf.setFontSize(9) + + const headers = ['Metode Pembayaran', 'Tipe', 'Jumlah Order', 'Total Amount', 'Persentase'] + currentX = marginLeft + + headers.forEach((header, index) => { + if (index === 0) { + pdf.text(header, currentX + 2, yPos + 6) + } else { + pdf.text(header, currentX + colWidths[index] / 2, yPos + 6, { align: 'center' }) + } + currentX += colWidths[index] + }) + + yPos += 12 + + // Table rows + pdf.setFont('helvetica', 'normal') + pdf.setFontSize(9) + + paymentData.data?.forEach((payment, index) => { + if (checkPageBreak(10)) yPos = 20 + + currentX = marginLeft + pdf.setFont('helvetica', 'normal') + pdf.setFontSize(9) + pdf.setTextColor(0, 0, 0) + + // Method name + pdf.text(payment.payment_method_name, currentX + 2, yPos + 5) + currentX += colWidths[0] + + // Type with simple color coding + const typeText = payment.payment_method_type.toUpperCase() + if (payment.payment_method_type === 'cash') { + pdf.setTextColor(0, 120, 0) // Green + } else if (payment.payment_method_type === 'card') { + pdf.setTextColor(0, 80, 200) // Blue + } else { + pdf.setTextColor(200, 100, 0) // Orange + } + pdf.text(typeText, currentX + colWidths[1] / 2, yPos + 5, { align: 'center' }) + pdf.setTextColor(0, 0, 0) // Reset color + currentX += colWidths[1] + + // Order count + pdf.text(payment.order_count.toString(), currentX + colWidths[2] / 2, yPos + 5, { align: 'center' }) + currentX += colWidths[2] + + // Amount + pdf.text(this.formatCurrency(payment.total_amount), currentX + colWidths[3] - 2, yPos + 5, { align: 'right' }) + currentX += colWidths[3] + + // Percentage + pdf.text(`${(payment.percentage ?? 0).toFixed(1)}%`, currentX + colWidths[4] / 2, yPos + 5, { align: 'center' }) + + // Draw bottom border line + pdf.setDrawColor(230, 230, 230) + pdf.setLineWidth(0.3) + pdf.line(marginLeft, yPos + 8, marginLeft + tableWidth, yPos + 8) + + yPos += 10 + }) + + // Table footer (Total) - directly after last row + pdf.setFillColor(245, 245, 245) // Lighter gray + pdf.rect(marginLeft, yPos, tableWidth, 10, 'F') + + pdf.setFont('helvetica', 'bold') + pdf.setFontSize(9) + + currentX = marginLeft + pdf.text('TOTAL', currentX + 2, yPos + 6) + currentX += colWidths[0] + colWidths[1] + + pdf.text((paymentData.summary?.total_orders ?? 0).toString(), currentX + colWidths[2] / 2, yPos + 6, { + align: 'center' + }) + currentX += colWidths[2] + + pdf.text(this.formatCurrency(paymentData.summary?.total_amount ?? 0), currentX + colWidths[3] - 2, yPos + 6, { + align: 'right' + }) + + currentX += colWidths[3] + pdf.text('100.0%', currentX + colWidths[4] / 2, yPos + 6, { align: 'center' }) + + return yPos + 20 + } + + /** + * Format currency for display + */ + private static formatCurrency(amount: number): string { + return `Rp ${amount.toLocaleString('id-ID')}` + } + + /** + * Generate filename with timestamp + */ + private static generateFilename(prefix: string, extension: string): string { + const now = new Date() + const year = now.getFullYear() + const month = (now.getMonth() + 1).toString().padStart(2, '0') + const day = now.getDate().toString().padStart(2, '0') + const hour = now.getHours().toString().padStart(2, '0') + const minute = now.getMinutes().toString().padStart(2, '0') + + return `${prefix}_${year}_${month}_${day}_${hour}${minute}.${extension}` + } + + /** + * Export Payment Method data with custom configuration + */ + static async exportCustomPaymentToPDF( + paymentData: PaymentReport, + options?: { + title?: string + includeSummary?: boolean + customFilename?: string + } + ) { + try { + const jsPDFModule = await import('jspdf') + const jsPDF = jsPDFModule.default + + const pdf = new jsPDF('p', 'mm', 'a4') + + let yPos = 20 + const pageWidth = pdf.internal.pageSize.getWidth() + const pageHeight = pdf.internal.pageSize.getHeight() + const marginLeft = 20 + const marginRight = 20 + const marginBottom = 15 + + const checkPageBreak = (neededSpace: number) => { + if (yPos + neededSpace > pageHeight - marginBottom) { + pdf.addPage() + yPos = 20 + return true + } + return false + } + + // Custom title if provided + if (options?.title) { + pdf.setFontSize(20) + pdf.setFont('helvetica', 'bold') + pdf.setTextColor(0, 0, 0) + pdf.text(options.title, pageWidth / 2, yPos, { align: 'center' }) + yPos += 15 + } else { + yPos = this.addReportTitle(pdf, paymentData, yPos, pageWidth, marginLeft, marginRight) + } + + // Optional summary section + if (options?.includeSummary !== false) { + checkPageBreak(50) + yPos = this.addRingkasanSection(pdf, paymentData, yPos, pageWidth, marginLeft, marginRight, checkPageBreak) + } + + // Payment methods detail + checkPageBreak(80) + yPos = this.addPaymentMethodsSection(pdf, paymentData, yPos, pageWidth, marginLeft, marginRight, checkPageBreak) + + const exportFilename = options?.customFilename || this.generateFilename('Custom_Payment_Report', 'pdf') + pdf.save(exportFilename) + + return { success: true, filename: exportFilename } + } catch (error) { + console.error('Error exporting custom payment report to PDF:', error) + return { success: false, error: `PDF export failed: ${(error as Error).message}` } + } + } +} diff --git a/src/views/apps/report/financial/payment-method-report/ReportPaymentMethodContent.tsx b/src/views/apps/report/financial/payment-method-report/ReportPaymentMethodContent.tsx index eaec83c..0516149 100644 --- a/src/views/apps/report/financial/payment-method-report/ReportPaymentMethodContent.tsx +++ b/src/views/apps/report/financial/payment-method-report/ReportPaymentMethodContent.tsx @@ -3,14 +3,16 @@ import DateRangePicker from '@/components/RangeDatePicker' import { ReportItemHeader, ReportItemSubheader } from '@/components/report/ReportItem' import { ExcelExportPaymentService } from '@/services/export/excel/ExcelExportPaymentService' +import { PDFExportPaymentService } from '@/services/export/pdf/PDFExportPaymentService' import { usePaymentAnalytics } from '@/services/queries/analytics' import { formatCurrency, formatDateDDMMYYYY } from '@/utils/transform' -import { Button, Card, CardContent } from '@mui/material' +import { Button, Card, CardContent, Menu, MenuItem } from '@mui/material' import { useState } from 'react' const ReportPaymentMethodContent = () => { const [startDate, setStartDate] = useState(new Date()) const [endDate, setEndDate] = useState(new Date()) + const [anchorEl, setAnchorEl] = useState(null) const { data: paymentAnalytics } = usePaymentAnalytics({ date_from: formatDateDDMMYYYY(startDate!), @@ -38,6 +40,38 @@ const ReportPaymentMethodContent = () => { } } + const handleExportPDF = async () => { + if (!paymentAnalytics) { + console.warn('No data available for export') + return + } + + try { + const result = await PDFExportPaymentService.exportPaymentMethodToPDF( + paymentAnalytics, + `Laporan_Metode_Pembayaran_${formatDateDDMMYYYY(startDate!)}_${formatDateDDMMYYYY(endDate!)}.pdf` + ) + + if (result.success) { + console.log(`PDF exported successfully: ${result.filename}`) + // Optional: Show success message to user + } else { + console.error('PDF export failed:', result.error) + // Optional: Show error message to user + } + } catch (error) { + console.error('PDF export error:', error) + } + } + + const handleExportClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget) + } + + const handleExportClose = () => { + setAnchorEl(null) + } + return (
@@ -46,11 +80,31 @@ const ReportPaymentMethodContent = () => { color='secondary' variant='tonal' startIcon={} + endIcon={} className='max-sm:is-full' - onClick={handleExportExcel} + onClick={handleExportClick} > Ekspor + + { + handleExportExcel() + handleExportClose() + }} + > + Export Excel + + { + handleExportPDF() + handleExportClose() + }} + > + + Export PDF + +