profit loss pdf
This commit is contained in:
parent
d317e8d06f
commit
e6bcf287ea
11
package-lock.json
generated
11
package-lock.json
generated
@ -52,6 +52,7 @@
|
||||
"emoji-mart": "5.6.0",
|
||||
"fs-extra": "11.2.0",
|
||||
"html2canvas": "^1.4.1",
|
||||
"html2pdf.js": "^0.12.1",
|
||||
"input-otp": "1.4.1",
|
||||
"jspdf": "^3.0.3",
|
||||
"jspdf-autotable": "^5.0.2",
|
||||
@ -7511,6 +7512,16 @@
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/html2pdf.js": {
|
||||
"version": "0.12.1",
|
||||
"resolved": "https://registry.npmjs.org/html2pdf.js/-/html2pdf.js-0.12.1.tgz",
|
||||
"integrity": "sha512-3rBWQ96H5oOU9jtoz3MnE/epGi27ig9h8aonBk4JTpvUERM3lMRxhIRckhJZEi4wE0YfRINoYOIDY0hLY0CHgQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"html2canvas": "^1.0.0",
|
||||
"jspdf": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/htmlparser2": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz",
|
||||
|
||||
@ -58,6 +58,7 @@
|
||||
"emoji-mart": "5.6.0",
|
||||
"fs-extra": "11.2.0",
|
||||
"html2canvas": "^1.4.1",
|
||||
"html2pdf.js": "^0.12.1",
|
||||
"input-otp": "1.4.1",
|
||||
"jspdf": "^3.0.3",
|
||||
"jspdf-autotable": "^5.0.2",
|
||||
|
||||
577
src/services/export/pdf/PDFExportProfitLossService.ts
Normal file
577
src/services/export/pdf/PDFExportProfitLossService.ts
Normal file
@ -0,0 +1,577 @@
|
||||
// services/pdfExportService.ts
|
||||
import type { ProfitLossReport } from '@/types/services/analytic'
|
||||
|
||||
export class PDFExportProfitLossService {
|
||||
/**
|
||||
* Export Profit Loss Report to PDF (Simple approach)
|
||||
*/
|
||||
static async exportProfitLossToPDF(profitData: ProfitLossReport, 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') // portrait
|
||||
|
||||
// Add content
|
||||
this.addBasicContent(pdf, profitData)
|
||||
|
||||
// Generate filename
|
||||
const exportFilename = filename || this.generateFilename('Laba_Rugi', 'pdf')
|
||||
|
||||
// Save PDF
|
||||
pdf.save(exportFilename)
|
||||
|
||||
return { success: true, filename: exportFilename }
|
||||
} catch (error) {
|
||||
console.error('Error exporting to PDF:', error)
|
||||
return { success: false, error: `PDF export failed: ${(error as Error).message}` }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add basic content to PDF with proper page management
|
||||
*/
|
||||
private static addBasicContent(pdf: any, profitData: ProfitLossReport) {
|
||||
let yPos = 20 // Reduced from 30
|
||||
const pageWidth = pdf.internal.pageSize.getWidth()
|
||||
const pageHeight = pdf.internal.pageSize.getHeight()
|
||||
const marginBottom = 15 // Reduced from 20
|
||||
|
||||
// Title - Center aligned
|
||||
pdf.setFontSize(18) // Reduced from 20
|
||||
pdf.setFont('helvetica', 'bold')
|
||||
pdf.text('Laporan Laba Rugi', pageWidth / 2, yPos, { align: 'center' })
|
||||
yPos += 10 // Reduced from 15
|
||||
|
||||
// Period - Center aligned
|
||||
pdf.setFontSize(11) // Reduced from 12
|
||||
pdf.setFont('helvetica', 'normal')
|
||||
const periodText = `${profitData.date_from.split('T')[0]} - ${profitData.date_to.split('T')[0]}`
|
||||
pdf.text(periodText, pageWidth / 2, yPos, { align: 'center' })
|
||||
yPos += 12 // Reduced from 20
|
||||
|
||||
// Purple line separator
|
||||
pdf.setDrawColor(102, 45, 145) // Purple color
|
||||
pdf.setLineWidth(1.5) // Reduced from 2
|
||||
pdf.line(20, yPos, pageWidth - 20, yPos)
|
||||
yPos += 15 // Reduced from 25
|
||||
|
||||
// Ringkasan section
|
||||
pdf.setFontSize(14) // Reduced from 16
|
||||
pdf.setFont('helvetica', 'bold')
|
||||
pdf.setTextColor(102, 45, 145) // Purple color
|
||||
pdf.text('Ringkasan', 20, yPos)
|
||||
yPos += 12 // Reduced from 20
|
||||
|
||||
// Reset text color to black
|
||||
pdf.setTextColor(0, 0, 0)
|
||||
pdf.setFont('helvetica', 'normal')
|
||||
pdf.setFontSize(11) // Reduced from 12
|
||||
|
||||
// Summary items with consistent spacing
|
||||
const summaryItems = [
|
||||
{ label: 'Total Penjualan', value: this.formatCurrency(profitData.summary.total_revenue) },
|
||||
{ label: 'Total Biaya', value: this.formatCurrency(profitData.summary.total_cost) },
|
||||
{ label: 'Total Diskon', value: this.formatCurrency(profitData.summary.total_discount) },
|
||||
{ label: 'Total Pajak', value: this.formatCurrency(profitData.summary.total_tax) },
|
||||
{ label: 'Laba Kotor', value: this.formatCurrency(profitData.summary.gross_profit) },
|
||||
{ label: 'Laba Bersih', value: this.formatCurrency(profitData.summary.net_profit) }
|
||||
]
|
||||
|
||||
summaryItems.forEach((item, index) => {
|
||||
// Add some spacing between items
|
||||
if (index > 0) yPos += 8 // Reduced from 12
|
||||
|
||||
// Check if we need new page for summary items
|
||||
if (yPos > pageHeight - marginBottom - 15) {
|
||||
pdf.addPage()
|
||||
yPos = 20 // Reduced from 30
|
||||
}
|
||||
|
||||
// Label on left
|
||||
pdf.text(item.label, 20, yPos)
|
||||
|
||||
// Value on right
|
||||
pdf.text(item.value, pageWidth - 20, yPos, { align: 'right' })
|
||||
|
||||
// Light gray line separator
|
||||
pdf.setDrawColor(230, 230, 230)
|
||||
pdf.setLineWidth(0.3)
|
||||
pdf.line(20, yPos + 3, pageWidth - 20, yPos + 3)
|
||||
})
|
||||
|
||||
yPos += 20 // Reduced from 30
|
||||
|
||||
// Check if we need new page before daily breakdown
|
||||
if (yPos > pageHeight - marginBottom - 40) {
|
||||
pdf.addPage()
|
||||
yPos = 20 // Reduced from 30
|
||||
}
|
||||
|
||||
// Daily breakdown section
|
||||
pdf.setFontSize(14) // Reduced from 16
|
||||
pdf.setFont('helvetica', 'bold')
|
||||
pdf.setTextColor(102, 45, 145) // Purple color
|
||||
pdf.text('Rincian Harian', 20, yPos)
|
||||
yPos += 12 // Reduced from 20
|
||||
|
||||
// Reset text color
|
||||
pdf.setTextColor(0, 0, 0)
|
||||
|
||||
// Create simple daily breakdown with page management
|
||||
profitData.data.forEach((daily, index) => {
|
||||
// Estimate space needed for this daily section (approx 100mm)
|
||||
const estimatedSpace = 100
|
||||
|
||||
// Check if we need new page before adding daily section
|
||||
if (yPos + estimatedSpace > pageHeight - marginBottom) {
|
||||
pdf.addPage()
|
||||
yPos = 20 // Reduced from 30
|
||||
}
|
||||
|
||||
yPos = this.addCleanDailySection(pdf, daily, yPos, pageWidth, pageHeight, marginBottom)
|
||||
yPos += 10 // Reduced from 15 - Space between daily sections
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Add clean daily section with page break management
|
||||
*/
|
||||
private static addCleanDailySection(
|
||||
pdf: any,
|
||||
dailyData: any,
|
||||
startY: number,
|
||||
pageWidth: number,
|
||||
pageHeight: number,
|
||||
marginBottom: number
|
||||
) {
|
||||
const date = new Date(dailyData.date).toLocaleDateString('id-ID', {
|
||||
day: '2-digit',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
})
|
||||
|
||||
let yPos = startY
|
||||
|
||||
// Check if we have enough space for the header
|
||||
if (yPos > pageHeight - marginBottom - 20) {
|
||||
pdf.addPage()
|
||||
yPos = 20 // Reduced from 30
|
||||
}
|
||||
|
||||
// Date header
|
||||
pdf.setFontSize(12) // Reduced from 14
|
||||
pdf.setFont('helvetica', 'bold')
|
||||
pdf.text(date, 20, yPos)
|
||||
yPos += 10 // Reduced from 15
|
||||
|
||||
// Daily data items
|
||||
pdf.setFontSize(10) // Reduced from 11
|
||||
pdf.setFont('helvetica', 'normal')
|
||||
|
||||
const dailyItems = [
|
||||
{ label: 'Penjualan', value: this.formatCurrency(dailyData.revenue) },
|
||||
{ label: 'HPP (Biaya Pokok)', value: this.formatCurrency(dailyData.cost) },
|
||||
{ label: 'Laba Kotor', value: this.formatCurrency(dailyData.gross_profit) },
|
||||
{ label: 'Pajak', value: this.formatCurrency(dailyData.tax) },
|
||||
{ label: 'Diskon', value: this.formatCurrency(dailyData.discount) },
|
||||
{ label: 'Jumlah Order', value: dailyData.orders.toString() + ' transaksi' },
|
||||
{ label: 'Laba Bersih', value: this.formatCurrency(dailyData.net_profit), isTotal: true }
|
||||
]
|
||||
|
||||
dailyItems.forEach((item, index) => {
|
||||
if (index > 0) yPos += 7 // Reduced from 10
|
||||
|
||||
// Check if we need new page for each item
|
||||
if (yPos > pageHeight - marginBottom - 15) {
|
||||
pdf.addPage()
|
||||
yPos = 20 // Reduced from 30
|
||||
}
|
||||
|
||||
// Special styling for total (Laba Bersih)
|
||||
if (item.isTotal) {
|
||||
pdf.setFont('helvetica', 'bold')
|
||||
|
||||
// Light background for total row - adjusted to center with text
|
||||
pdf.setFillColor(248, 248, 248)
|
||||
pdf.rect(20, yPos - 4, pageWidth - 40, 9, 'F') // Slightly bigger and better positioned
|
||||
} else {
|
||||
pdf.setFont('helvetica', 'normal')
|
||||
}
|
||||
|
||||
// Label on left - consistent with other rows
|
||||
pdf.text(item.label, 25, yPos)
|
||||
|
||||
// Value on right - consistent with other rows
|
||||
pdf.text(item.value, pageWidth - 25, yPos, { align: 'right' })
|
||||
|
||||
// Subtle line separator (except for last item)
|
||||
if (index < dailyItems.length - 1) {
|
||||
pdf.setDrawColor(245, 245, 245)
|
||||
pdf.setLineWidth(0.2)
|
||||
pdf.line(25, yPos + 1.5, pageWidth - 25, yPos + 1.5) // Reduced from yPos + 2
|
||||
}
|
||||
})
|
||||
|
||||
return yPos + 6 // Reduced from 8
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Alternative: More precise page break management
|
||||
*/
|
||||
static async exportWithBetterPageBreaks(profitData: ProfitLossReport, filename?: string) {
|
||||
try {
|
||||
const jsPDFModule = await import('jspdf')
|
||||
const jsPDF = jsPDFModule.default
|
||||
const pdf = new jsPDF('p', 'mm', 'a4')
|
||||
|
||||
// Use more precise measurements
|
||||
const pageHeight = pdf.internal.pageSize.getHeight() // ~297mm for A4
|
||||
const safeHeight = pageHeight - 30 // Keep 30mm margin from bottom
|
||||
|
||||
let currentY = 30
|
||||
|
||||
// Helper function to check and add new page
|
||||
const checkPageBreak = (neededSpace: number) => {
|
||||
if (currentY + neededSpace > safeHeight) {
|
||||
pdf.addPage()
|
||||
currentY = 30
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Add title and header
|
||||
currentY = this.addTitleSection(pdf, profitData, currentY)
|
||||
|
||||
// Add summary with page break check
|
||||
checkPageBreak(80) // Estimate 80mm needed for summary
|
||||
currentY = this.addSummarySection(pdf, profitData, currentY, checkPageBreak)
|
||||
|
||||
// Add daily breakdown
|
||||
checkPageBreak(40) // Space for section header
|
||||
currentY = this.addDailyBreakdownSection(pdf, profitData, currentY, checkPageBreak)
|
||||
|
||||
const exportFilename = filename || this.generateFilename('Laba_Rugi', 'pdf')
|
||||
pdf.save(exportFilename)
|
||||
|
||||
return { success: true, filename: exportFilename }
|
||||
} catch (error) {
|
||||
console.error('Error exporting to PDF:', error)
|
||||
return { success: false, error: `PDF export failed: ${(error as Error).message}` }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add title section
|
||||
*/
|
||||
private static addTitleSection(pdf: any, profitData: ProfitLossReport, startY: number): number {
|
||||
let yPos = startY
|
||||
const pageWidth = pdf.internal.pageSize.getWidth()
|
||||
|
||||
// Title
|
||||
pdf.setFontSize(20)
|
||||
pdf.setFont('helvetica', 'bold')
|
||||
pdf.text('Laporan Laba Rugi', pageWidth / 2, yPos, { align: 'center' })
|
||||
yPos += 15
|
||||
|
||||
// Period
|
||||
pdf.setFontSize(12)
|
||||
pdf.setFont('helvetica', 'normal')
|
||||
const periodText = `${profitData.date_from.split('T')[0]} - ${profitData.date_to.split('T')[0]}`
|
||||
pdf.text(periodText, pageWidth / 2, yPos, { align: 'center' })
|
||||
yPos += 20
|
||||
|
||||
// Separator line
|
||||
pdf.setDrawColor(102, 45, 145)
|
||||
pdf.setLineWidth(2)
|
||||
pdf.line(20, yPos, pageWidth - 20, yPos)
|
||||
yPos += 25
|
||||
|
||||
return yPos
|
||||
}
|
||||
|
||||
/**
|
||||
* Add summary section with page break callback
|
||||
*/
|
||||
private static addSummarySection(
|
||||
pdf: any,
|
||||
profitData: ProfitLossReport,
|
||||
startY: number,
|
||||
checkPageBreak: (space: number) => boolean
|
||||
): number {
|
||||
let yPos = startY
|
||||
const pageWidth = pdf.internal.pageSize.getWidth()
|
||||
|
||||
// Section title
|
||||
pdf.setFontSize(16)
|
||||
pdf.setFont('helvetica', 'bold')
|
||||
pdf.setTextColor(102, 45, 145)
|
||||
pdf.text('Ringkasan', 20, yPos)
|
||||
yPos += 20
|
||||
|
||||
// Reset formatting
|
||||
pdf.setTextColor(0, 0, 0)
|
||||
pdf.setFont('helvetica', 'normal')
|
||||
pdf.setFontSize(12)
|
||||
|
||||
const summaryItems = [
|
||||
{ label: 'Total Penjualan', value: this.formatCurrency(profitData.summary.total_revenue) },
|
||||
{ label: 'Total Biaya', value: this.formatCurrency(profitData.summary.total_cost) },
|
||||
{ label: 'Total Diskon', value: this.formatCurrency(profitData.summary.total_discount) },
|
||||
{ label: 'Total Pajak', value: this.formatCurrency(profitData.summary.total_tax) },
|
||||
{ label: 'Laba Kotor', value: this.formatCurrency(profitData.summary.gross_profit) },
|
||||
{ label: 'Laba Bersih', value: this.formatCurrency(profitData.summary.net_profit) }
|
||||
]
|
||||
|
||||
summaryItems.forEach((item, index) => {
|
||||
if (index > 0) yPos += 12
|
||||
|
||||
// Check page break for each item
|
||||
if (checkPageBreak(15)) {
|
||||
yPos = 30
|
||||
}
|
||||
|
||||
pdf.text(item.label, 20, yPos)
|
||||
pdf.text(item.value, pageWidth - 20, yPos, { align: 'right' })
|
||||
|
||||
// Separator line
|
||||
pdf.setDrawColor(230, 230, 230)
|
||||
pdf.setLineWidth(0.3)
|
||||
pdf.line(20, yPos + 3, pageWidth - 20, yPos + 3)
|
||||
})
|
||||
|
||||
return yPos + 30
|
||||
}
|
||||
|
||||
/**
|
||||
* Add daily breakdown section with page break management
|
||||
*/
|
||||
private static addDailyBreakdownSection(
|
||||
pdf: any,
|
||||
profitData: ProfitLossReport,
|
||||
startY: number,
|
||||
checkPageBreak: (space: number) => boolean
|
||||
): number {
|
||||
let yPos = startY
|
||||
|
||||
// Section title
|
||||
pdf.setFontSize(16)
|
||||
pdf.setFont('helvetica', 'bold')
|
||||
pdf.setTextColor(102, 45, 145)
|
||||
pdf.text('Rincian Harian', 20, yPos)
|
||||
yPos += 20
|
||||
|
||||
pdf.setTextColor(0, 0, 0)
|
||||
|
||||
profitData.data.forEach((daily, index) => {
|
||||
// Check if we need space for this daily section (estimate ~90mm)
|
||||
if (checkPageBreak(90)) {
|
||||
yPos = 30
|
||||
}
|
||||
|
||||
yPos = this.addSingleDayData(pdf, daily, yPos)
|
||||
yPos += 15
|
||||
})
|
||||
|
||||
return yPos
|
||||
}
|
||||
|
||||
/**
|
||||
* Add single day data
|
||||
*/
|
||||
private static addSingleDayData(pdf: any, dailyData: any, startY: number): number {
|
||||
const pageWidth = pdf.internal.pageSize.getWidth()
|
||||
let yPos = startY
|
||||
|
||||
const date = new Date(dailyData.date).toLocaleDateString('id-ID', {
|
||||
day: '2-digit',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
})
|
||||
|
||||
// Date header
|
||||
pdf.setFontSize(14)
|
||||
pdf.setFont('helvetica', 'bold')
|
||||
pdf.text(date, 20, yPos)
|
||||
yPos += 15
|
||||
|
||||
// Daily items
|
||||
pdf.setFontSize(11)
|
||||
pdf.setFont('helvetica', 'normal')
|
||||
|
||||
const items = [
|
||||
{ label: 'Penjualan', value: this.formatCurrency(dailyData.revenue) },
|
||||
{ label: 'HPP (Biaya Pokok)', value: this.formatCurrency(dailyData.cost) },
|
||||
{ label: 'Laba Kotor', value: this.formatCurrency(dailyData.gross_profit) },
|
||||
{ label: 'Pajak', value: this.formatCurrency(dailyData.tax) },
|
||||
{ label: 'Diskon', value: this.formatCurrency(dailyData.discount) },
|
||||
{ label: 'Jumlah Order', value: dailyData.orders.toString() + ' transaksi' },
|
||||
{ label: 'Laba Bersih', value: this.formatCurrency(dailyData.net_profit), isTotal: true }
|
||||
]
|
||||
|
||||
items.forEach((item, index) => {
|
||||
if (index > 0) yPos += 10
|
||||
|
||||
if (item.isTotal) {
|
||||
pdf.setFont('helvetica', 'bold')
|
||||
pdf.setFillColor(248, 248, 248)
|
||||
pdf.rect(20, yPos - 4, pageWidth - 40, 10, 'F')
|
||||
} else {
|
||||
pdf.setFont('helvetica', 'normal')
|
||||
}
|
||||
|
||||
pdf.text(item.label, 25, yPos)
|
||||
pdf.text(item.value, pageWidth - 25, yPos, { align: 'right' })
|
||||
|
||||
if (index < items.length - 1) {
|
||||
pdf.setDrawColor(245, 245, 245)
|
||||
pdf.setLineWidth(0.2)
|
||||
pdf.line(25, yPos + 2, pageWidth - 25, yPos + 2)
|
||||
}
|
||||
})
|
||||
|
||||
return yPos + 8
|
||||
}
|
||||
|
||||
/**
|
||||
* Alternative HTML to PDF method (if needed)
|
||||
*/
|
||||
static async exportToHTMLPDF(profitData: ProfitLossReport, filename?: string) {
|
||||
try {
|
||||
const htmlContent = this.generateSimpleHTML(profitData)
|
||||
|
||||
// Create a temporary element and trigger print
|
||||
const printWindow = window.open('', '_blank')
|
||||
if (printWindow) {
|
||||
printWindow.document.write(htmlContent)
|
||||
printWindow.document.close()
|
||||
printWindow.focus()
|
||||
printWindow.print()
|
||||
printWindow.close()
|
||||
}
|
||||
|
||||
return { success: true, filename: filename || 'Laba_Rugi.pdf' }
|
||||
} catch (error) {
|
||||
return { success: false, error: `HTML PDF export failed: ${(error as Error).message}` }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate simple HTML for printing
|
||||
*/
|
||||
private static generateSimpleHTML(profitData: ProfitLossReport): string {
|
||||
const dateColumns = profitData.data.map(daily => {
|
||||
const date = new Date(daily.date)
|
||||
return date.toLocaleDateString('id-ID', { day: '2-digit', month: 'short' })
|
||||
})
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Laporan Laba Rugi</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 20px; }
|
||||
h1 { color: #1f4e79; text-align: center; }
|
||||
h2 { color: #333; border-bottom: 2px solid #ccc; }
|
||||
.summary { margin-bottom: 30px; }
|
||||
.summary-item { margin: 5px 0; padding: 5px; background: #f9f9f9; }
|
||||
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
|
||||
th, td { border: 1px solid #ccc; padding: 8px; text-align: left; }
|
||||
th { background-color: #d35400; color: white; text-align: center; }
|
||||
.number { text-align: right; }
|
||||
.center { text-align: center; }
|
||||
@media print {
|
||||
body { margin: 10px; }
|
||||
.page-break { page-break-before: always; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>LAPORAN LABA RUGI</h1>
|
||||
<p style="text-align: center;">
|
||||
Periode: ${profitData.date_from.split('T')[0]} - ${profitData.date_to.split('T')[0]}
|
||||
</p>
|
||||
|
||||
<h2>RINGKASAN PERIODE</h2>
|
||||
<div class="summary">
|
||||
<div class="summary-item">Total Revenue: <strong>${this.formatCurrency(profitData.summary.total_revenue)}</strong></div>
|
||||
<div class="summary-item">Total Cost: <strong>${this.formatCurrency(profitData.summary.total_cost)}</strong></div>
|
||||
<div class="summary-item">Gross Profit: <strong>${this.formatCurrency(profitData.summary.gross_profit)}</strong></div>
|
||||
<div class="summary-item">Net Profit: <strong>${this.formatCurrency(profitData.summary.net_profit)}</strong></div>
|
||||
<div class="summary-item">Total Orders: <strong>${profitData.summary.total_orders}</strong></div>
|
||||
</div>
|
||||
|
||||
<div class="page-break"></div>
|
||||
<h2>RINCIAN HARIAN</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>NO</th>
|
||||
<th>KETERANGAN</th>
|
||||
<th></th>
|
||||
${dateColumns.map(date => `<th>${date}</th>`).join('')}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="center">1</td>
|
||||
<td><strong>TOTAL PENJ</strong></td>
|
||||
<td class="center">:</td>
|
||||
${profitData.data.map(daily => `<td class="number">${this.formatCurrency(daily.revenue)}</td>`).join('')}
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="center">2</td>
|
||||
<td><strong>HPP</strong></td>
|
||||
<td class="center">:</td>
|
||||
${profitData.data.map(daily => `<td class="number">${this.formatCurrency(daily.cost)}</td>`).join('')}
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="center">3</td>
|
||||
<td><strong>Laba Kotor</strong></td>
|
||||
<td class="center">:</td>
|
||||
${profitData.data.map(daily => `<td class="number">${this.formatCurrency(daily.gross_profit)}</td>`).join('')}
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="center">4</td>
|
||||
<td><strong>Biaya lain</strong></td>
|
||||
<td class="center">:</td>
|
||||
${profitData.data.map(daily => `<td class="number">${this.formatCurrency(daily.tax + daily.discount)}</td>`).join('')}
|
||||
</tr>
|
||||
<tr style="background-color: #154360; color: white; font-weight: bold;">
|
||||
<td class="center">5</td>
|
||||
<td>Laba/Rugi</td>
|
||||
<td class="center">:</td>
|
||||
${profitData.data.map(daily => `<td class="number">${this.formatCurrency(daily.net_profit)}</td>`).join('')}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
}
|
||||
}
|
||||
@ -3,8 +3,11 @@
|
||||
import DateRangePicker from '@/components/RangeDatePicker'
|
||||
import { ReportItem, ReportItemFooter, ReportItemHeader, ReportItemSubheader } from '@/components/report/ReportItem'
|
||||
import { ExcelExportProfitLossService } from '@/services/export/excel/ExcelExportProfitLossService'
|
||||
import { PDFExportProfitLossService } from '@/services/export/pdf/PDFExportProfitLossService'
|
||||
import { ProfitLossReport } from '@/types/services/analytic'
|
||||
import { Button, Card, CardContent, Box } from '@mui/material'
|
||||
import { Button, Card, CardContent, Box, Menu, MenuItem, ListItemIcon, ListItemText } from '@mui/material'
|
||||
|
||||
import { useState } from 'react'
|
||||
|
||||
interface ReportProfitLossContentProps {
|
||||
profitData: ProfitLossReport | undefined
|
||||
@ -31,23 +34,52 @@ const ReportProfitLossContent = ({
|
||||
onStartDateChange,
|
||||
onEndDateChange
|
||||
}: ReportProfitLossContentProps) => {
|
||||
const handleExport = async () => {
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)
|
||||
const open = Boolean(anchorEl)
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setAnchorEl(event.currentTarget)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null)
|
||||
}
|
||||
const handleExportExcel = async () => {
|
||||
if (!profitData) return
|
||||
handleClose()
|
||||
|
||||
try {
|
||||
const result = await ExcelExportProfitLossService.exportProfitLossToExcel(profitData)
|
||||
|
||||
if (result.success) {
|
||||
// Optional: Show success notification
|
||||
console.log('Export successful:', result.filename)
|
||||
// You can add toast notification here
|
||||
console.log('Excel export successful:', result.filename)
|
||||
} else {
|
||||
console.error('Export failed:', result.error)
|
||||
alert('Export gagal. Silakan coba lagi.')
|
||||
console.error('Excel export failed:', result.error)
|
||||
alert('Export Excel gagal. Silakan coba lagi.')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Export error:', error)
|
||||
alert('Terjadi kesalahan saat export.')
|
||||
console.error('Excel export error:', error)
|
||||
alert('Terjadi kesalahan saat export Excel.')
|
||||
}
|
||||
}
|
||||
|
||||
const handleExportPDF = async () => {
|
||||
if (!profitData) return
|
||||
handleClose()
|
||||
|
||||
try {
|
||||
const result = await PDFExportProfitLossService.exportProfitLossToPDF(profitData)
|
||||
|
||||
if (result.success) {
|
||||
console.log('PDF export successful:', result.filename)
|
||||
// Optional: Show success notification
|
||||
} else {
|
||||
console.error('PDF export failed:', result.error)
|
||||
alert('Export PDF gagal. Silakan coba lagi.')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('PDF export error:', error)
|
||||
alert('Terjadi kesalahan saat export PDF.')
|
||||
}
|
||||
}
|
||||
|
||||
@ -58,13 +90,43 @@ const ReportProfitLossContent = ({
|
||||
<Button
|
||||
color='secondary'
|
||||
variant='tonal'
|
||||
startIcon={<i className='tabler-upload' />}
|
||||
startIcon={<i className='tabler-download' />}
|
||||
endIcon={<i className='tabler-chevron-down' />}
|
||||
className='max-sm:is-full'
|
||||
onClick={handleExport}
|
||||
onClick={handleClick}
|
||||
disabled={!profitData}
|
||||
aria-controls={open ? 'export-menu' : undefined}
|
||||
aria-haspopup='true'
|
||||
aria-expanded={open ? 'true' : undefined}
|
||||
>
|
||||
Ekspor
|
||||
Export
|
||||
</Button>
|
||||
|
||||
<Menu
|
||||
id='export-menu'
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
MenuListProps={{
|
||||
'aria-labelledby': 'export-button'
|
||||
}}
|
||||
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
|
||||
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
|
||||
>
|
||||
<MenuItem onClick={handleExportExcel}>
|
||||
<ListItemIcon>
|
||||
<i className='tabler-file-type-xls text-green-600' />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Export to Excel</ListItemText>
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem onClick={handleExportPDF}>
|
||||
<ListItemIcon>
|
||||
<i className='tabler-file-type-pdf text-red-600' />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Export to PDF</ListItemText>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
<DateRangePicker
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user