Sales Report PDF
This commit is contained in:
@@ -0,0 +1,686 @@
|
||||
import { CategoryReport, PaymentReport, ProductSalesReport, ProfitLossReport } from '@/types/services/analytic'
|
||||
|
||||
export interface SalesReportData {
|
||||
profitLoss: ProfitLossReport
|
||||
paymentAnalytics: PaymentReport
|
||||
categoryAnalytics: CategoryReport
|
||||
productAnalytics: ProductSalesReport
|
||||
}
|
||||
|
||||
export class PDFExportSalesService {
|
||||
/**
|
||||
* Export Sales Report to PDF
|
||||
*/
|
||||
static async exportSalesReportToPDF(salesData: SalesReportData, 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.addSalesReportContent(pdf, salesData)
|
||||
|
||||
// Generate filename
|
||||
const exportFilename = filename || this.generateFilename('Laporan_Transaksi', 'pdf')
|
||||
|
||||
// Save PDF
|
||||
pdf.save(exportFilename)
|
||||
|
||||
return { success: true, filename: exportFilename }
|
||||
} catch (error) {
|
||||
console.error('Error exporting sales report to PDF:', error)
|
||||
return { success: false, error: `PDF export failed: ${(error as Error).message}` }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add sales report content to PDF
|
||||
*/
|
||||
private static async addSalesReportContent(pdf: any, salesData: SalesReportData) {
|
||||
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, salesData.profitLoss, yPos, pageWidth, marginLeft, marginRight)
|
||||
|
||||
// Section 1: Ringkasan
|
||||
checkPageBreak(60)
|
||||
yPos = this.addRingkasanSection(pdf, salesData.profitLoss, yPos, pageWidth, marginLeft, marginRight, checkPageBreak)
|
||||
|
||||
// Section 2: Invoice Summary
|
||||
checkPageBreak(50)
|
||||
yPos = this.addInvoiceSection(pdf, salesData.profitLoss, yPos, pageWidth, marginLeft, marginRight, checkPageBreak)
|
||||
|
||||
// Section 3: Payment Methods
|
||||
checkPageBreak(120)
|
||||
yPos = this.addPaymentMethodsSection(
|
||||
pdf,
|
||||
salesData.paymentAnalytics,
|
||||
yPos,
|
||||
pageWidth,
|
||||
marginLeft,
|
||||
marginRight,
|
||||
checkPageBreak
|
||||
)
|
||||
|
||||
// Section 4: Category Summary
|
||||
checkPageBreak(120)
|
||||
yPos = this.addCategorySection(
|
||||
pdf,
|
||||
salesData.categoryAnalytics,
|
||||
yPos,
|
||||
pageWidth,
|
||||
marginLeft,
|
||||
marginRight,
|
||||
checkPageBreak
|
||||
)
|
||||
|
||||
// Section 5: Product Summary
|
||||
checkPageBreak(150)
|
||||
yPos = this.addProductSection(
|
||||
pdf,
|
||||
salesData.productAnalytics,
|
||||
yPos,
|
||||
pageWidth,
|
||||
marginLeft,
|
||||
marginRight,
|
||||
checkPageBreak
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add report title
|
||||
*/
|
||||
private static addReportTitle(
|
||||
pdf: any,
|
||||
profitLoss: ProfitLossReport,
|
||||
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 Transaksi', pageWidth / 2, yPos, { align: 'center' })
|
||||
yPos += 10
|
||||
|
||||
// Period
|
||||
pdf.setFontSize(12)
|
||||
pdf.setFont('helvetica', 'normal')
|
||||
const periodText = `${profitLoss.date_from.split('T')[0]} - ${profitLoss.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
|
||||
*/
|
||||
private static addRingkasanSection(
|
||||
pdf: any,
|
||||
profitLoss: ProfitLossReport,
|
||||
startY: number,
|
||||
pageWidth: number,
|
||||
marginLeft: number,
|
||||
marginRight: 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('Ringkasan', marginLeft, yPos)
|
||||
yPos += 10
|
||||
|
||||
// Reset text color
|
||||
pdf.setTextColor(0, 0, 0)
|
||||
pdf.setFontSize(11)
|
||||
|
||||
const ringkasanItems = [
|
||||
{ label: 'Total Penjualan', value: this.formatCurrency(profitLoss.summary.total_revenue), bold: false },
|
||||
{ label: 'Total Diskon', value: this.formatCurrency(profitLoss.summary.total_discount), bold: false },
|
||||
{ label: 'Total Pajak', value: this.formatCurrency(profitLoss.summary.total_tax), bold: false },
|
||||
{ label: 'Total', value: this.formatCurrency(profitLoss.summary.total_revenue), 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 total 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 + 10
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Invoice section
|
||||
*/
|
||||
private static addInvoiceSection(
|
||||
pdf: any,
|
||||
profitLoss: ProfitLossReport,
|
||||
startY: number,
|
||||
pageWidth: number,
|
||||
marginLeft: number,
|
||||
marginRight: 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('Invoice', marginLeft, yPos)
|
||||
yPos += 10
|
||||
|
||||
// Reset formatting
|
||||
pdf.setTextColor(0, 0, 0)
|
||||
pdf.setFontSize(11)
|
||||
|
||||
const invoiceItems = [
|
||||
{ label: 'Total Invoice', value: profitLoss.summary.total_orders.toString() },
|
||||
{ label: 'Rata-rata Tagihan Per Invoice', value: this.formatCurrency(profitLoss.summary.average_profit) }
|
||||
]
|
||||
|
||||
invoiceItems.forEach((item, index) => {
|
||||
if (checkPageBreak(15)) yPos = 20
|
||||
|
||||
pdf.setFont('helvetica', 'normal')
|
||||
pdf.text(item.label, marginLeft, yPos)
|
||||
pdf.text(item.value, pageWidth - marginRight, yPos, { align: 'right' })
|
||||
|
||||
// Light separator line
|
||||
if (index < invoiceItems.length - 1) {
|
||||
pdf.setDrawColor(230, 230, 230)
|
||||
pdf.setLineWidth(0.5)
|
||||
pdf.line(marginLeft, yPos + 2, pageWidth - marginRight, yPos + 2)
|
||||
}
|
||||
|
||||
yPos += 8
|
||||
})
|
||||
|
||||
return yPos + 15
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Payment Methods section
|
||||
*/
|
||||
private static addPaymentMethodsSection(
|
||||
pdf: any,
|
||||
paymentAnalytics: 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('Ringkasan 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)
|
||||
|
||||
paymentAnalytics.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((paymentAnalytics.summary?.total_orders ?? 0).toString(), currentX + colWidths[2] / 2, yPos + 6, {
|
||||
align: 'center'
|
||||
})
|
||||
currentX += colWidths[2]
|
||||
|
||||
pdf.text(this.formatCurrency(paymentAnalytics.summary?.total_amount ?? 0), currentX + colWidths[3] - 2, yPos + 6, {
|
||||
align: 'right'
|
||||
})
|
||||
|
||||
return yPos + 25
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Category section
|
||||
*/
|
||||
private static addCategorySection(
|
||||
pdf: any,
|
||||
categoryAnalytics: CategoryReport,
|
||||
startY: number,
|
||||
pageWidth: number,
|
||||
marginLeft: number,
|
||||
marginRight: 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('Ringkasan Kategori', marginLeft, yPos)
|
||||
yPos += 15
|
||||
|
||||
// Reset formatting
|
||||
pdf.setTextColor(0, 0, 0)
|
||||
|
||||
// Table setup
|
||||
const tableWidth = pageWidth - marginLeft - marginRight
|
||||
const colWidths = [50, 30, 25, 35] // Name, Products, Qty, Revenue
|
||||
let currentX = marginLeft
|
||||
|
||||
// Table header
|
||||
pdf.setFillColor(240, 240, 240)
|
||||
pdf.rect(marginLeft, yPos - 3, tableWidth, 12, 'F')
|
||||
|
||||
pdf.setFont('helvetica', 'bold')
|
||||
pdf.setFontSize(10)
|
||||
|
||||
const headers = ['Nama', 'Total Produk', 'Qty', 'Pendapatan']
|
||||
currentX = marginLeft
|
||||
|
||||
headers.forEach((header, index) => {
|
||||
if (index === 0) {
|
||||
pdf.text(header, currentX + 2, yPos + 3)
|
||||
} else {
|
||||
pdf.text(header, currentX + colWidths[index] - 2, yPos + 3, { align: 'right' })
|
||||
}
|
||||
currentX += colWidths[index]
|
||||
})
|
||||
|
||||
yPos += 15
|
||||
|
||||
// Calculate summaries
|
||||
const categorySummary = {
|
||||
totalRevenue: categoryAnalytics.data?.reduce((sum, item) => sum + (item?.total_revenue || 0), 0) || 0,
|
||||
productCount: categoryAnalytics.data?.reduce((sum, item) => sum + (item?.product_count || 0), 0) || 0,
|
||||
totalQuantity: categoryAnalytics.data?.reduce((sum, item) => sum + (item?.total_quantity || 0), 0) || 0
|
||||
}
|
||||
|
||||
// Table rows
|
||||
pdf.setFont('helvetica', 'normal')
|
||||
pdf.setFontSize(9)
|
||||
|
||||
categoryAnalytics.data?.forEach((category, index) => {
|
||||
if (checkPageBreak(12)) yPos = 20
|
||||
|
||||
// Row background (alternating)
|
||||
if (index % 2 === 1) {
|
||||
pdf.setFillColor(250, 250, 250)
|
||||
pdf.rect(marginLeft, yPos - 3, tableWidth, 10, 'F')
|
||||
}
|
||||
|
||||
currentX = marginLeft
|
||||
|
||||
// Category name
|
||||
pdf.text(category.category_name, currentX + 2, yPos + 2)
|
||||
currentX += colWidths[0]
|
||||
|
||||
// Product count
|
||||
pdf.text(category.product_count.toString(), currentX + colWidths[1] - 2, yPos + 2, { align: 'right' })
|
||||
currentX += colWidths[1]
|
||||
|
||||
// Quantity
|
||||
pdf.text(category.total_quantity.toString(), currentX + colWidths[2] - 2, yPos + 2, { align: 'right' })
|
||||
currentX += colWidths[2]
|
||||
|
||||
// Revenue
|
||||
pdf.text(this.formatCurrency(category.total_revenue), currentX + colWidths[3] - 2, yPos + 2, { align: 'right' })
|
||||
|
||||
yPos += 10
|
||||
})
|
||||
|
||||
// Table footer (Total)
|
||||
if (checkPageBreak(12)) yPos = 20
|
||||
|
||||
pdf.setFillColor(220, 220, 220)
|
||||
pdf.rect(marginLeft, yPos - 3, tableWidth, 12, 'F')
|
||||
|
||||
pdf.setFont('helvetica', 'bold')
|
||||
pdf.setFontSize(10)
|
||||
|
||||
currentX = marginLeft
|
||||
pdf.text('TOTAL', currentX + 2, yPos + 3)
|
||||
currentX += colWidths[0]
|
||||
|
||||
pdf.text(categorySummary.productCount.toString(), currentX + colWidths[1] - 2, yPos + 3, { align: 'right' })
|
||||
currentX += colWidths[1]
|
||||
|
||||
pdf.text(categorySummary.totalQuantity.toString(), currentX + colWidths[2] - 2, yPos + 3, { align: 'right' })
|
||||
currentX += colWidths[2]
|
||||
|
||||
pdf.text(this.formatCurrency(categorySummary.totalRevenue), currentX + colWidths[3] - 2, yPos + 3, {
|
||||
align: 'right'
|
||||
})
|
||||
|
||||
return yPos + 25
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Product section with category grouping
|
||||
*/
|
||||
private static addProductSection(
|
||||
pdf: any,
|
||||
productAnalytics: ProductSalesReport,
|
||||
startY: number,
|
||||
pageWidth: number,
|
||||
marginLeft: number,
|
||||
marginRight: 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('Ringkasan Item', marginLeft, yPos)
|
||||
yPos += 15
|
||||
|
||||
// Reset formatting
|
||||
pdf.setTextColor(0, 0, 0)
|
||||
|
||||
// Table setup
|
||||
const tableWidth = pageWidth - marginLeft - marginRight
|
||||
const colWidths = [60, 20, 20, 30, 30] // Product, Qty, Order, Revenue, Average
|
||||
let currentX = marginLeft
|
||||
|
||||
// Table header
|
||||
pdf.setFillColor(240, 240, 240)
|
||||
pdf.rect(marginLeft, yPos - 3, tableWidth, 12, 'F')
|
||||
|
||||
pdf.setFont('helvetica', 'bold')
|
||||
pdf.setFontSize(10)
|
||||
|
||||
const headers = ['Produk', 'Qty', 'Order', 'Pendapatan', 'Rata Rata']
|
||||
currentX = marginLeft
|
||||
|
||||
headers.forEach((header, index) => {
|
||||
if (index === 0) {
|
||||
pdf.text(header, currentX + 2, yPos + 3)
|
||||
} else {
|
||||
pdf.text(header, currentX + colWidths[index] - 2, yPos + 3, { align: 'right' })
|
||||
}
|
||||
currentX += colWidths[index]
|
||||
})
|
||||
|
||||
yPos += 15
|
||||
|
||||
// Group products by category
|
||||
const groupedProducts =
|
||||
productAnalytics.data?.reduce(
|
||||
(acc, item) => {
|
||||
const categoryName = item.category_name || 'Tidak Berkategori'
|
||||
if (!acc[categoryName]) {
|
||||
acc[categoryName] = []
|
||||
}
|
||||
acc[categoryName].push(item)
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, any[]>
|
||||
) || {}
|
||||
|
||||
// Calculate product summary
|
||||
const productSummary = {
|
||||
totalQuantitySold: productAnalytics.data?.reduce((sum, item) => sum + (item?.quantity_sold || 0), 0) || 0,
|
||||
totalRevenue: productAnalytics.data?.reduce((sum, item) => sum + (item?.revenue || 0), 0) || 0,
|
||||
totalOrders: productAnalytics.data?.reduce((sum, item) => sum + (item?.order_count || 0), 0) || 0
|
||||
}
|
||||
|
||||
pdf.setFontSize(9)
|
||||
|
||||
// Render grouped products
|
||||
Object.keys(groupedProducts)
|
||||
.sort()
|
||||
.forEach(categoryName => {
|
||||
const categoryProducts = groupedProducts[categoryName]
|
||||
|
||||
// Check page break for category header
|
||||
if (checkPageBreak(15)) yPos = 20
|
||||
|
||||
// Category header
|
||||
pdf.setFillColor(230, 230, 230)
|
||||
pdf.rect(marginLeft, yPos - 3, tableWidth, 12, 'F')
|
||||
pdf.setFont('helvetica', 'bold')
|
||||
pdf.setTextColor(102, 45, 145)
|
||||
pdf.text(categoryName.toUpperCase(), marginLeft + 2, yPos + 3)
|
||||
pdf.setTextColor(0, 0, 0)
|
||||
yPos += 12
|
||||
|
||||
// Category products
|
||||
categoryProducts.forEach((item, index) => {
|
||||
if (checkPageBreak(10)) yPos = 20
|
||||
|
||||
// Row background (alternating within category)
|
||||
if (index % 2 === 1) {
|
||||
pdf.setFillColor(250, 250, 250)
|
||||
pdf.rect(marginLeft, yPos - 2, tableWidth, 8, 'F')
|
||||
}
|
||||
|
||||
pdf.setFont('helvetica', 'normal')
|
||||
currentX = marginLeft
|
||||
|
||||
// Product name (indented and truncated if needed)
|
||||
const productName =
|
||||
item.product_name.length > 45 ? item.product_name.substring(0, 42) + '...' : item.product_name
|
||||
|
||||
pdf.text(productName, currentX + 5, yPos + 2) // Indented for products
|
||||
currentX += colWidths[0]
|
||||
|
||||
// Quantity
|
||||
pdf.text(item.quantity_sold.toString(), currentX + colWidths[1] - 2, yPos + 2, { align: 'right' })
|
||||
currentX += colWidths[1]
|
||||
|
||||
// Order count
|
||||
pdf.text((item.order_count || 0).toString(), currentX + colWidths[2] - 2, yPos + 2, { align: 'right' })
|
||||
currentX += colWidths[2]
|
||||
|
||||
// Revenue
|
||||
pdf.text(this.formatCurrency(item.revenue), currentX + colWidths[3] - 2, yPos + 2, { align: 'right' })
|
||||
currentX += colWidths[3]
|
||||
|
||||
// Average price
|
||||
pdf.text(this.formatCurrency(item.average_price), currentX + colWidths[4] - 2, yPos + 2, { align: 'right' })
|
||||
|
||||
yPos += 8
|
||||
})
|
||||
|
||||
// Category subtotal
|
||||
if (checkPageBreak(10)) yPos = 20
|
||||
|
||||
const categoryTotalQty = categoryProducts.reduce((sum, item) => sum + (item.quantity_sold || 0), 0)
|
||||
const categoryTotalOrders = categoryProducts.reduce((sum, item) => sum + (item.order_count || 0), 0)
|
||||
const categoryTotalRevenue = categoryProducts.reduce((sum, item) => sum + (item.revenue || 0), 0)
|
||||
|
||||
pdf.setFillColor(200, 200, 200)
|
||||
pdf.rect(marginLeft, yPos - 2, tableWidth, 10, 'F')
|
||||
pdf.setFont('helvetica', 'bold')
|
||||
|
||||
currentX = marginLeft
|
||||
pdf.text(`Subtotal ${categoryName}`, currentX + 5, yPos + 3)
|
||||
currentX += colWidths[0]
|
||||
|
||||
pdf.text(categoryTotalQty.toString(), currentX + colWidths[1] - 2, yPos + 3, { align: 'right' })
|
||||
currentX += colWidths[1]
|
||||
|
||||
pdf.text(categoryTotalOrders.toString(), currentX + colWidths[2] - 2, yPos + 3, { align: 'right' })
|
||||
currentX += colWidths[2]
|
||||
|
||||
pdf.text(this.formatCurrency(categoryTotalRevenue), currentX + colWidths[3] - 2, yPos + 3, { align: 'right' })
|
||||
|
||||
yPos += 15
|
||||
})
|
||||
|
||||
// Grand total
|
||||
if (checkPageBreak(12)) yPos = 20
|
||||
|
||||
pdf.setFillColor(180, 180, 180)
|
||||
pdf.rect(marginLeft, yPos - 3, tableWidth, 12, 'F')
|
||||
|
||||
pdf.setFont('helvetica', 'bold')
|
||||
pdf.setFontSize(10)
|
||||
|
||||
currentX = marginLeft
|
||||
pdf.text('TOTAL KESELURUHAN', currentX + 2, yPos + 3)
|
||||
currentX += colWidths[0]
|
||||
|
||||
pdf.text(productSummary.totalQuantitySold.toString(), currentX + colWidths[1] - 2, yPos + 3, { align: 'right' })
|
||||
currentX += colWidths[1]
|
||||
|
||||
pdf.text(productSummary.totalOrders.toString(), currentX + colWidths[2] - 2, yPos + 3, { align: 'right' })
|
||||
currentX += colWidths[2]
|
||||
|
||||
pdf.text(this.formatCurrency(productSummary.totalRevenue), currentX + colWidths[3] - 2, yPos + 3, {
|
||||
align: 'right'
|
||||
})
|
||||
|
||||
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}`
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user