Report Sales Order

This commit is contained in:
efrilm 2025-09-26 13:47:04 +07:00
parent c3ad938a79
commit 82e79463b5
3 changed files with 940 additions and 2 deletions

View File

@ -0,0 +1,335 @@
// services/excelExportSalesOrderService.ts
import type { SalesReport } from '@/types/services/analytic'
export class ExcelExportSalesOrderService {
/**
* Export Sales Order Report to Excel
*/
static async exportSalesOrderToExcel(salesData: SalesReport, filename?: string) {
try {
// Dynamic import untuk xlsx library
const XLSX = await import('xlsx')
// Prepare data untuk Excel
const worksheetData: any[][] = []
// Header dengan report info (baris 1-2)
worksheetData.push(['LAPORAN PESANAN PENJUALAN']) // Row 0 - Main title
worksheetData.push([`Periode: ${salesData.date_from.split('T')[0]} - ${salesData.date_to.split('T')[0]}`]) // Row 1 - Period
worksheetData.push([]) // Empty row
// Add Summary Section
worksheetData.push(['RINGKASAN PERIODE']) // Section header
worksheetData.push([]) // Empty row
const summaryData = [
['Total Sales:', `Rp ${salesData.summary.total_sales.toLocaleString('id-ID')}`],
['Total Orders:', salesData.summary.total_orders.toString()],
['Total Items:', salesData.summary.total_items.toString()],
['Average Order Value:', `Rp ${salesData.summary.average_order_value.toLocaleString('id-ID')}`],
['Total Tax:', `Rp ${salesData.summary.total_tax.toLocaleString('id-ID')}`],
['Total Discount:', `Rp ${salesData.summary.total_discount.toLocaleString('id-ID')}`],
['Net Sales:', `Rp ${salesData.summary.net_sales.toLocaleString('id-ID')}`]
]
summaryData.forEach(row => {
worksheetData.push([row[0], row[1]]) // Only 2 columns needed
})
worksheetData.push([]) // Empty row
worksheetData.push([]) // Empty row
// Daily Sales Section Header
worksheetData.push(['RINCIAN HARIAN']) // Section header
worksheetData.push([]) // Empty row
// Header row untuk tabel daily sales data
const headerRow = ['No', 'Tanggal', 'Penjualan', 'Pesanan', 'Qty', 'Pajak', 'Diskon', 'Pendapatan']
worksheetData.push(headerRow)
// Add daily sales data rows
salesData.data?.forEach((dailySales, index) => {
const rowData = [
index + 1, // No
this.formatDate(dailySales.date),
dailySales.sales, // Store as number for Excel formatting
dailySales.orders,
dailySales.items,
dailySales.tax, // Store as number for Excel formatting
dailySales.discount, // Store as number for Excel formatting
dailySales.net_sales // Store as number for Excel formatting
]
worksheetData.push(rowData)
})
// Add total row
const totalRow = [
'TOTAL',
'',
salesData.summary.total_sales,
salesData.summary.total_orders,
salesData.summary.total_items,
salesData.summary.total_tax,
salesData.summary.total_discount,
salesData.summary.net_sales
]
worksheetData.push(totalRow)
// Create workbook dan worksheet
const workbook = XLSX.utils.book_new()
const worksheet = XLSX.utils.aoa_to_sheet(worksheetData)
// Apply basic formatting
this.applyBasicFormatting(worksheet, worksheetData.length, XLSX)
// Add worksheet ke workbook
XLSX.utils.book_append_sheet(workbook, worksheet, 'Pesanan Penjualan')
// Generate filename
const exportFilename = filename || this.generateFilename('Pesanan_Penjualan')
// Download file
XLSX.writeFile(workbook, exportFilename)
return { success: true, filename: exportFilename }
} catch (error) {
console.error('Error exporting to Excel:', error)
return { success: false, error: 'Failed to export Excel file' }
}
}
/**
* Apply basic formatting (SheetJS compatible)
*/
private static applyBasicFormatting(worksheet: any, totalRows: number, XLSX: any) {
// Set column widths
const colWidths = [
{ wch: 8 }, // No
{ wch: 15 }, // Tanggal
{ wch: 18 }, // Penjualan
{ wch: 12 }, // Pesanan
{ wch: 10 }, // Qty
{ wch: 15 }, // Pajak
{ wch: 15 }, // Diskon
{ wch: 18 } // Pendapatan
]
worksheet['!cols'] = colWidths
// Set row heights for better spacing
worksheet['!rows'] = [
{ hpt: 30 }, // Title row
{ hpt: 25 }, // Period row
{ hpt: 15 }, // Empty row
{ hpt: 25 }, // Summary header
{ hpt: 15 } // Empty row
]
// Merge cells untuk headers
const merges = [
{ s: { r: 0, c: 0 }, e: { r: 0, c: 7 } }, // Title (span across all columns)
{ s: { r: 1, c: 0 }, e: { r: 1, c: 7 } }, // Period (span across all columns)
{ s: { r: 3, c: 0 }, e: { r: 3, c: 7 } } // Summary header (span across all columns)
]
// Find and add merge for daily sales header
for (let i = 0; i < totalRows; i++) {
const cell = worksheet[XLSX.utils.encode_cell({ r: i, c: 0 })]
if (cell && cell.v === 'RINCIAN HARIAN') {
merges.push({ s: { r: i, c: 0 }, e: { r: i, c: 7 } }) // Span across all columns
break
}
}
worksheet['!merges'] = merges
// Apply number formatting untuk currency cells
this.applyNumberFormatting(worksheet, totalRows, XLSX)
}
/**
* Apply number formatting for currency and styling
*/
private static applyNumberFormatting(worksheet: any, totalRows: number, XLSX: any) {
// Find table data start (after header row)
let dataStartRow = -1
for (let i = 0; i < totalRows; i++) {
const cell = worksheet[XLSX.utils.encode_cell({ r: i, c: 0 })]
if (cell && cell.v === 'No') {
dataStartRow = i + 1
break
}
}
if (dataStartRow === -1) return
// Count actual data rows (excluding total row)
const dataRowsCount = totalRows - dataStartRow - 1 // -1 for total row
// Apply currency formatting to currency columns (columns 2, 5, 6, 7 - Penjualan, Pajak, Diskon, Pendapatan)
const currencyColumns = [2, 5, 6, 7]
for (let row = dataStartRow; row <= dataStartRow + dataRowsCount; row++) {
// Include total row
currencyColumns.forEach(col => {
const cellAddress = XLSX.utils.encode_cell({ r: row, c: col })
const cell = worksheet[cellAddress]
if (cell && typeof cell.v === 'number') {
// Apply Indonesian currency format
cell.z = '#,##0'
cell.t = 'n'
}
})
}
// Apply styling to header row
const headerRowIndex = dataStartRow - 1
for (let col = 0; col < 8; col++) {
const cellAddress = XLSX.utils.encode_cell({ r: headerRowIndex, c: col })
const cell = worksheet[cellAddress]
if (cell) {
// Apply bold formatting (basic approach for SheetJS)
cell.s = {
font: { bold: true },
fill: { fgColor: { rgb: 'F3F4F6' } }, // Light gray background
border: {
bottom: { style: 'medium', color: { rgb: '000000' } }
}
}
}
}
// Apply styling to total row
const totalRowIndex = dataStartRow + dataRowsCount
for (let col = 0; col < 8; col++) {
const cellAddress = XLSX.utils.encode_cell({ r: totalRowIndex, c: col })
const cell = worksheet[cellAddress]
if (cell) {
// Apply bold formatting for total row
cell.s = {
font: { bold: true },
border: {
top: { style: 'medium', color: { rgb: '000000' } }
}
}
}
}
}
/**
* Format date for display
*/
private static formatDate(dateString: string): string {
const date = new Date(dateString)
return date.toLocaleDateString('id-ID', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
})
}
/**
* Generate filename with timestamp
*/
private static generateFilename(prefix: 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}.xlsx`
}
/**
* Export Sales Order data with custom configuration
*/
static async exportCustomSalesOrderData(
salesData: SalesReport,
options?: {
includeSummary?: boolean
includeItemsColumn?: boolean
customFilename?: string
sheetName?: string
}
) {
try {
const XLSX = await import('xlsx')
const worksheetData: any[][] = []
// Always include title and period
worksheetData.push(['LAPORAN PESANAN PENJUALAN'])
worksheetData.push([`Periode: ${salesData.date_from.split('T')[0]} - ${salesData.date_to.split('T')[0]}`])
worksheetData.push([])
// Optional summary
if (options?.includeSummary !== false) {
worksheetData.push(['RINGKASAN PERIODE'])
worksheetData.push([])
const summaryData = [
['Total Sales:', `Rp ${salesData.summary.total_sales.toLocaleString('id-ID')}`],
['Total Orders:', salesData.summary.total_orders.toString()],
['Total Items:', salesData.summary.total_items.toString()],
['Net Sales:', `Rp ${salesData.summary.net_sales.toLocaleString('id-ID')}`]
]
summaryData.forEach(row => worksheetData.push([row[0], row[1]]))
worksheetData.push([])
worksheetData.push([])
}
worksheetData.push(['RINCIAN HARIAN'])
worksheetData.push([])
// Header row based on options
const headerRow =
options?.includeItemsColumn !== false
? ['No', 'Tanggal', 'Penjualan', 'Pesanan', 'Qty', 'Pajak', 'Diskon', 'Pendapatan']
: ['No', 'Tanggal', 'Penjualan', 'Pesanan', 'Pajak', 'Diskon', 'Pendapatan']
worksheetData.push(headerRow)
// Add daily data based on options
salesData.data?.forEach((dailySales, index) => {
const rowData =
options?.includeItemsColumn !== false
? [
index + 1,
this.formatDate(dailySales.date),
dailySales.sales,
dailySales.orders,
dailySales.items,
dailySales.tax,
dailySales.discount,
dailySales.net_sales
]
: [
index + 1,
this.formatDate(dailySales.date),
dailySales.sales,
dailySales.orders,
dailySales.tax,
dailySales.discount,
dailySales.net_sales
]
worksheetData.push(rowData)
})
const workbook = XLSX.utils.book_new()
const worksheet = XLSX.utils.aoa_to_sheet(worksheetData)
this.applyBasicFormatting(worksheet, worksheetData.length, XLSX)
const sheetName = options?.sheetName || 'Pesanan Penjualan'
XLSX.utils.book_append_sheet(workbook, worksheet, sheetName)
const exportFilename = options?.customFilename || this.generateFilename('Custom_Sales_Order')
XLSX.writeFile(workbook, exportFilename)
return { success: true, filename: exportFilename }
} catch (error) {
console.error('Error exporting custom sales order data to Excel:', error)
return { success: false, error: 'Failed to export Excel file' }
}
}
}

View File

@ -0,0 +1,529 @@
// services/pdfExportSalesOrderService.ts
import { SalesReport } from '@/types/services/analytic'
export class PDFExportSalesOrderService {
/**
* Export Sales Order Report to PDF
*/
static async exportSalesOrderToPDF(salesData: SalesReport, 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.addSalesOrderReportContent(pdf, salesData)
// Generate filename
const exportFilename = filename || this.generateFilename('Laporan_Pesanan_Penjualan', 'pdf')
// Save PDF
pdf.save(exportFilename)
return { success: true, filename: exportFilename }
} catch (error) {
console.error('Error exporting sales order report to PDF:', error)
return { success: false, error: `PDF export failed: ${(error as Error).message}` }
}
}
/**
* Add sales order report content to PDF
*/
private static async addSalesOrderReportContent(pdf: any, salesData: SalesReport) {
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, yPos, pageWidth, marginLeft, marginRight)
// Section 1: Ringkasan
checkPageBreak(60)
yPos = this.addRingkasanSection(pdf, salesData, yPos, pageWidth, marginLeft, marginRight, checkPageBreak)
// Section 2: Daily Sales Details
checkPageBreak(100)
yPos = this.addDailySalesSection(pdf, salesData, yPos, pageWidth, marginLeft, marginRight, checkPageBreak)
}
/**
* Add report title
*/
private static addReportTitle(
pdf: any,
salesData: SalesReport,
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 Pesanan Penjualan', pageWidth / 2, yPos, { align: 'center' })
yPos += 10
// Period
pdf.setFontSize(12)
pdf.setFont('helvetica', 'normal')
const periodText = `${salesData.date_from.split('T')[0]} - ${salesData.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,
salesData: SalesReport,
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', marginLeft, yPos)
yPos += 12
// Reset text color
pdf.setTextColor(0, 0, 0)
pdf.setFontSize(11)
const ringkasanItems = [
{ label: 'Total Sales', value: this.formatCurrency(salesData.summary.total_sales), bold: false },
{ label: 'Total Orders', value: salesData.summary.total_orders.toString(), bold: false },
{ label: 'Total Items', value: salesData.summary.total_items.toString(), bold: false },
{ label: 'Average Order Value', value: this.formatCurrency(salesData.summary.average_order_value), bold: false },
{ label: 'Total Tax', value: this.formatCurrency(salesData.summary.total_tax), bold: false },
{ label: 'Total Discount', value: this.formatCurrency(salesData.summary.total_discount), bold: false },
{ label: 'Net Sales', value: this.formatCurrency(salesData.summary.net_sales), 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 bold 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 Daily Sales section
*/
private static addDailySalesSection(
pdf: any,
salesData: SalesReport,
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 Harian', marginLeft, yPos)
yPos += 12
// Reset formatting
pdf.setTextColor(0, 0, 0)
// Table setup - adjust for 7 columns
const tableWidth = pageWidth - marginLeft - marginRight
const colWidths = [20, 20, 25, 15, 20, 20, 25] // Date, Sales, Orders, Items, Tax, Discount, Net Sales
let currentX = marginLeft
// Table header
pdf.setFillColor(240, 240, 240)
pdf.rect(marginLeft, yPos, tableWidth, 10, 'F')
pdf.setFont('helvetica', 'bold')
pdf.setFontSize(8) // Smaller font for more columns
const headers = ['Tanggal', 'Penjualan', 'Pesanan', 'Qty', 'Pajak', 'Diskon', 'Pendapatan']
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(8)
salesData.data?.forEach((dailySales, index) => {
if (checkPageBreak(10)) yPos = 20
currentX = marginLeft
pdf.setFont('helvetica', 'normal')
pdf.setFontSize(8)
pdf.setTextColor(0, 0, 0)
// Date
pdf.text(this.formatDate(dailySales.date), currentX + 2, yPos + 5)
currentX += colWidths[0]
// Sales
pdf.text(this.formatCurrencyShort(dailySales.sales), currentX + colWidths[1] - 2, yPos + 5, { align: 'right' })
currentX += colWidths[1]
// Orders
pdf.text(dailySales.orders.toString(), currentX + colWidths[2] / 2, yPos + 5, { align: 'center' })
currentX += colWidths[2]
// Items
pdf.text(dailySales.items.toString(), currentX + colWidths[3] / 2, yPos + 5, { align: 'center' })
currentX += colWidths[3]
// Tax
pdf.text(this.formatCurrencyShort(dailySales.tax), currentX + colWidths[4] - 2, yPos + 5, { align: 'right' })
currentX += colWidths[4]
// Discount
pdf.text(this.formatCurrencyShort(dailySales.discount), currentX + colWidths[5] - 2, yPos + 5, { align: 'right' })
currentX += colWidths[5]
// Net Sales
pdf.text(this.formatCurrencyShort(dailySales.net_sales), currentX + colWidths[6] - 2, yPos + 5, {
align: 'right'
})
// 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)
pdf.setFillColor(245, 245, 245) // Lighter gray
pdf.rect(marginLeft, yPos, tableWidth, 10, 'F')
pdf.setFont('helvetica', 'bold')
pdf.setFontSize(8)
currentX = marginLeft
pdf.text('TOTAL', currentX + 2, yPos + 6)
currentX += colWidths[0]
pdf.text(this.formatCurrencyShort(salesData.summary.total_sales), currentX + colWidths[1] - 2, yPos + 6, {
align: 'right'
})
currentX += colWidths[1]
pdf.text(salesData.summary.total_orders.toString(), currentX + colWidths[2] / 2, yPos + 6, { align: 'center' })
currentX += colWidths[2]
pdf.text(salesData.summary.total_items.toString(), currentX + colWidths[3] / 2, yPos + 6, { align: 'center' })
currentX += colWidths[3]
pdf.text(this.formatCurrencyShort(salesData.summary.total_tax), currentX + colWidths[4] - 2, yPos + 6, {
align: 'right'
})
currentX += colWidths[4]
pdf.text(this.formatCurrencyShort(salesData.summary.total_discount), currentX + colWidths[5] - 2, yPos + 6, {
align: 'right'
})
currentX += colWidths[5]
pdf.text(this.formatCurrencyShort(salesData.summary.net_sales), currentX + colWidths[6] - 2, yPos + 6, {
align: 'right'
})
return yPos + 20
}
/**
* Format currency for display
*/
private static formatCurrency(amount: number): string {
return `Rp ${amount.toLocaleString('id-ID')}`
}
/**
* Format currency short for table display
*/
private static formatCurrencyShort(amount: number): string {
if (amount >= 1000000) {
return `${(amount / 1000000).toFixed(1)}M`
} else if (amount >= 1000) {
return `${(amount / 1000).toFixed(0)}K`
} else {
return amount.toString()
}
}
/**
* Format date for display
*/
private static formatDate(dateString: string): string {
const date = new Date(dateString)
return date.toLocaleDateString('id-ID', {
day: '2-digit',
month: '2-digit'
})
}
/**
* 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 Sales Order data with custom configuration
*/
static async exportCustomSalesOrderToPDF(
salesData: SalesReport,
options?: {
title?: string
includeSummary?: boolean
customFilename?: string
compactMode?: boolean
}
) {
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, salesData, yPos, pageWidth, marginLeft, marginRight)
}
// Optional summary section
if (options?.includeSummary !== false) {
checkPageBreak(60)
yPos = this.addRingkasanSection(pdf, salesData, yPos, pageWidth, marginLeft, marginRight, checkPageBreak)
}
// Daily sales details
checkPageBreak(100)
if (options?.compactMode) {
yPos = this.addCompactDailySalesSection(
pdf,
salesData,
yPos,
pageWidth,
marginLeft,
marginRight,
checkPageBreak
)
} else {
yPos = this.addDailySalesSection(pdf, salesData, yPos, pageWidth, marginLeft, marginRight, checkPageBreak)
}
const exportFilename = options?.customFilename || this.generateFilename('Custom_Sales_Order', 'pdf')
pdf.save(exportFilename)
return { success: true, filename: exportFilename }
} catch (error) {
console.error('Error exporting custom sales order report to PDF:', error)
return { success: false, error: `PDF export failed: ${(error as Error).message}` }
}
}
/**
* Add Compact Daily Sales section (fewer columns)
*/
private static addCompactDailySalesSection(
pdf: any,
salesData: SalesReport,
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 Harian (Compact)', marginLeft, yPos)
yPos += 12
// Reset formatting
pdf.setTextColor(0, 0, 0)
// Table setup - compact with 4 columns
const tableWidth = pageWidth - marginLeft - marginRight
const colWidths = [30, 40, 30, 40] // Date, Sales, Orders, Net Sales
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 = ['Tanggal', 'Penjualan', 'Pesanan', 'Pendapatan']
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)
salesData.data?.forEach((dailySales, index) => {
if (checkPageBreak(10)) yPos = 20
currentX = marginLeft
pdf.setFont('helvetica', 'normal')
pdf.setFontSize(9)
pdf.setTextColor(0, 0, 0)
// Date
pdf.text(this.formatDate(dailySales.date), currentX + 2, yPos + 5)
currentX += colWidths[0]
// Sales
pdf.text(this.formatCurrency(dailySales.sales), currentX + colWidths[1] - 2, yPos + 5, { align: 'right' })
currentX += colWidths[1]
// Orders
pdf.text(dailySales.orders.toString(), currentX + colWidths[2] / 2, yPos + 5, { align: 'center' })
currentX += colWidths[2]
// Net Sales
pdf.text(this.formatCurrency(dailySales.net_sales), currentX + colWidths[3] - 2, yPos + 5, { align: 'right' })
// 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)
pdf.setFillColor(245, 245, 245)
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]
pdf.text(this.formatCurrency(salesData.summary.total_sales), currentX + colWidths[1] - 2, yPos + 6, {
align: 'right'
})
currentX += colWidths[1]
pdf.text(salesData.summary.total_orders.toString(), currentX + colWidths[2] / 2, yPos + 6, { align: 'center' })
currentX += colWidths[2]
pdf.text(this.formatCurrency(salesData.summary.net_sales), currentX + colWidths[3] - 2, yPos + 6, {
align: 'right'
})
return yPos + 20
}
}

View File

@ -4,19 +4,74 @@ import DateRangePicker from '@/components/RangeDatePicker'
import { ReportItemHeader, ReportItemSubheader } from '@/components/report/ReportItem'
import { useSalesAnalytics } from '@/services/queries/analytics'
import { formatCurrency, formatDate, 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 ReportSalesOrderCard from './ReportSalesOrderCard'
import { PDFExportSalesOrderService } from '@/services/export/pdf/PDFExportSalesOrderService'
import { ExcelExportSalesOrderService } from '@/services/export/excel/ExcelExportSalesOrderService'
const ReportSalesOrderContent = () => {
const [startDate, setStartDate] = useState<Date | null>(new Date())
const [endDate, setEndDate] = useState<Date | null>(new Date())
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)
const { data: sales } = useSalesAnalytics({
date_from: formatDateDDMMYYYY(startDate!),
date_to: formatDateDDMMYYYY(endDate!)
})
const handleExportClick = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget)
}
const handleExportClose = () => {
setAnchorEl(null)
}
const handleExportExcel = async () => {
if (!sales) {
console.warn('No data available for export')
return
}
try {
const result = await ExcelExportSalesOrderService.exportSalesOrderToExcel(
sales,
`Pesanan_Penjualan_${formatDateDDMMYYYY(startDate!)}_${formatDateDDMMYYYY(endDate!)}.xlsx`
)
if (result.success) {
console.log(`Excel exported successfully: ${result.filename}`)
} else {
console.error('Export failed:', result.error)
}
} catch (error) {
console.error('Export error:', error)
}
}
const handleExportPDF = async () => {
if (!sales) {
console.warn('No data available for export')
return
}
try {
const result = await PDFExportSalesOrderService.exportSalesOrderToPDF(
sales,
`Laporan_Pesanan_Penjualan_${formatDateDDMMYYYY(startDate!)}_${formatDateDDMMYYYY(endDate!)}.pdf`
)
if (result.success) {
console.log(`PDF exported successfully: ${result.filename}`)
} else {
console.error('Export failed:', result.error)
}
} catch (error) {
console.error('Export error:', error)
}
}
return (
<>
<ReportSalesOrderCard sales={sales} />
@ -28,11 +83,30 @@ const ReportSalesOrderContent = () => {
color='secondary'
variant='tonal'
startIcon={<i className='tabler-upload' />}
endIcon={<i className='tabler-chevron-down' />}
className='max-sm:is-full'
// onClick={handleExportPDF}
onClick={handleExportClick}
>
Ekspor
</Button>
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleExportClose}>
<MenuItem
onClick={() => {
handleExportExcel()
handleExportClose()
}}
>
Export Excel
</MenuItem>
<MenuItem
onClick={() => {
handleExportPDF()
handleExportClose()
}}
>
Export PDF
</MenuItem>
</Menu>
<DateRangePicker
startDate={startDate}
endDate={endDate}