PDF Export Payment Method
This commit is contained in:
parent
52879b58fe
commit
ce9120e7e6
364
src/services/export/pdf/PDFExportPaymentService.ts
Normal file
364
src/services/export/pdf/PDFExportPaymentService.ts
Normal file
@ -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}` }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,14 +3,16 @@
|
|||||||
import DateRangePicker from '@/components/RangeDatePicker'
|
import DateRangePicker from '@/components/RangeDatePicker'
|
||||||
import { ReportItemHeader, ReportItemSubheader } from '@/components/report/ReportItem'
|
import { ReportItemHeader, ReportItemSubheader } from '@/components/report/ReportItem'
|
||||||
import { ExcelExportPaymentService } from '@/services/export/excel/ExcelExportPaymentService'
|
import { ExcelExportPaymentService } from '@/services/export/excel/ExcelExportPaymentService'
|
||||||
|
import { PDFExportPaymentService } from '@/services/export/pdf/PDFExportPaymentService'
|
||||||
import { usePaymentAnalytics } from '@/services/queries/analytics'
|
import { usePaymentAnalytics } from '@/services/queries/analytics'
|
||||||
import { formatCurrency, formatDateDDMMYYYY } from '@/utils/transform'
|
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'
|
import { useState } from 'react'
|
||||||
|
|
||||||
const ReportPaymentMethodContent = () => {
|
const ReportPaymentMethodContent = () => {
|
||||||
const [startDate, setStartDate] = useState<Date | null>(new Date())
|
const [startDate, setStartDate] = useState<Date | null>(new Date())
|
||||||
const [endDate, setEndDate] = useState<Date | null>(new Date())
|
const [endDate, setEndDate] = useState<Date | null>(new Date())
|
||||||
|
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)
|
||||||
|
|
||||||
const { data: paymentAnalytics } = usePaymentAnalytics({
|
const { data: paymentAnalytics } = usePaymentAnalytics({
|
||||||
date_from: formatDateDDMMYYYY(startDate!),
|
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<HTMLElement>) => {
|
||||||
|
setAnchorEl(event.currentTarget)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleExportClose = () => {
|
||||||
|
setAnchorEl(null)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<div className='p-6 border-be'>
|
<div className='p-6 border-be'>
|
||||||
@ -46,11 +80,31 @@ const ReportPaymentMethodContent = () => {
|
|||||||
color='secondary'
|
color='secondary'
|
||||||
variant='tonal'
|
variant='tonal'
|
||||||
startIcon={<i className='tabler-upload' />}
|
startIcon={<i className='tabler-upload' />}
|
||||||
|
endIcon={<i className='tabler-chevron-down' />}
|
||||||
className='max-sm:is-full'
|
className='max-sm:is-full'
|
||||||
onClick={handleExportExcel}
|
onClick={handleExportClick}
|
||||||
>
|
>
|
||||||
Ekspor
|
Ekspor
|
||||||
</Button>
|
</Button>
|
||||||
|
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleExportClose}>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
handleExportExcel()
|
||||||
|
handleExportClose()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Export Excel
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
handleExportPDF()
|
||||||
|
handleExportClose()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<i className='tabler-file-pdf mr-2' />
|
||||||
|
Export PDF
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
<DateRangePicker
|
<DateRangePicker
|
||||||
startDate={startDate}
|
startDate={startDate}
|
||||||
endDate={endDate}
|
endDate={endDate}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user