Compare commits
No commits in common. "82e79463b51526d2c148eb73006f368045e96143" and "69c49238d45b058af4792d484910b13fe0ab9264" have entirely different histories.
82e79463b5
...
69c49238d4
@ -1,335 +0,0 @@
|
||||
// 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' }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,325 +0,0 @@
|
||||
// services/excelExportCategoryService.ts
|
||||
import type { CategoryReport } from '@/types/services/analytic'
|
||||
|
||||
export class ExcelExportSalesProductCategoryService {
|
||||
/**
|
||||
* Export Category Sales Report to Excel
|
||||
*/
|
||||
static async exportCategorySalesToExcel(categoryData: CategoryReport, 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 PENJUALAN KATEGORI']) // Row 0 - Main title
|
||||
worksheetData.push([`Periode: ${categoryData.date_from.split('T')[0]} - ${categoryData.date_to.split('T')[0]}`]) // Row 1 - Period
|
||||
worksheetData.push([]) // Empty row
|
||||
|
||||
// Calculate summary
|
||||
const categorySummary = {
|
||||
totalRevenue: categoryData.data?.reduce((sum, item) => sum + (item?.total_revenue || 0), 0) || 0,
|
||||
orderCount: categoryData.data?.reduce((sum, item) => sum + (item?.order_count || 0), 0) || 0,
|
||||
productCount: categoryData.data?.reduce((sum, item) => sum + (item?.product_count || 0), 0) || 0,
|
||||
totalQuantity: categoryData.data?.reduce((sum, item) => sum + (item?.total_quantity || 0), 0) || 0,
|
||||
totalCategories: categoryData.data?.length || 0
|
||||
}
|
||||
|
||||
// Add Summary Section
|
||||
worksheetData.push(['RINGKASAN PERIODE']) // Section header
|
||||
worksheetData.push([]) // Empty row
|
||||
|
||||
const summaryData = [
|
||||
['Total Kategori:', categorySummary.totalCategories.toString()],
|
||||
['Total Produk:', categorySummary.productCount.toString()],
|
||||
['Total Quantity:', categorySummary.totalQuantity.toString()],
|
||||
['Total Orders:', categorySummary.orderCount.toString()],
|
||||
['Total Revenue:', `Rp ${categorySummary.totalRevenue.toLocaleString('id-ID')}`]
|
||||
]
|
||||
|
||||
summaryData.forEach(row => {
|
||||
worksheetData.push([row[0], row[1]]) // Only 2 columns needed
|
||||
})
|
||||
|
||||
worksheetData.push([]) // Empty row
|
||||
worksheetData.push([]) // Empty row
|
||||
|
||||
// Category Details Section Header
|
||||
worksheetData.push(['RINCIAN KATEGORI']) // Section header
|
||||
worksheetData.push([]) // Empty row
|
||||
|
||||
// Header row untuk tabel category data
|
||||
const headerRow = ['No', 'Nama', 'Total Produk', 'Qty', 'Total Orders', 'Pendapatan']
|
||||
worksheetData.push(headerRow)
|
||||
|
||||
// Add category data rows
|
||||
categoryData.data?.forEach((category, index) => {
|
||||
const rowData = [
|
||||
index + 1, // No
|
||||
category.category_name,
|
||||
category.product_count,
|
||||
category.total_quantity,
|
||||
category.order_count,
|
||||
category.total_revenue // Store as number for Excel formatting
|
||||
]
|
||||
worksheetData.push(rowData)
|
||||
})
|
||||
|
||||
// Add total row
|
||||
const totalRow = [
|
||||
'TOTAL',
|
||||
'',
|
||||
categorySummary.productCount,
|
||||
categorySummary.totalQuantity,
|
||||
categorySummary.orderCount,
|
||||
categorySummary.totalRevenue
|
||||
]
|
||||
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, 'Penjualan Kategori')
|
||||
|
||||
// Generate filename
|
||||
const exportFilename = filename || this.generateFilename('Penjualan_Kategori')
|
||||
|
||||
// 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: 30 }, // Nama
|
||||
{ wch: 15 }, // Total Produk
|
||||
{ wch: 12 }, // Qty
|
||||
{ wch: 15 }, // Total Orders
|
||||
{ wch: 20 } // 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: 5 } }, // Title (span across all columns)
|
||||
{ s: { r: 1, c: 0 }, e: { r: 1, c: 5 } }, // Period (span across all columns)
|
||||
{ s: { r: 3, c: 0 }, e: { r: 3, c: 5 } } // Summary header (span across all columns)
|
||||
]
|
||||
|
||||
// Find and add merge for category details header
|
||||
for (let i = 0; i < totalRows; i++) {
|
||||
const cell = worksheet[XLSX.utils.encode_cell({ r: i, c: 0 })]
|
||||
if (cell && cell.v === 'RINCIAN KATEGORI') {
|
||||
merges.push({ s: { r: i, c: 0 }, e: { r: i, c: 5 } }) // 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 Pendapatan column (column 5 - index 5)
|
||||
for (let row = dataStartRow; row <= dataStartRow + dataRowsCount; row++) {
|
||||
// Include total row
|
||||
const cellAddress = XLSX.utils.encode_cell({ r: row, c: 5 }) // Pendapatan column
|
||||
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 < 6; 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 < 6; 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' } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 Category Sales data with custom configuration
|
||||
*/
|
||||
static async exportCustomCategoryData(
|
||||
categoryData: CategoryReport,
|
||||
options?: {
|
||||
includeSummary?: boolean
|
||||
includeOrderCount?: boolean
|
||||
customFilename?: string
|
||||
sheetName?: string
|
||||
}
|
||||
) {
|
||||
try {
|
||||
const XLSX = await import('xlsx')
|
||||
const worksheetData: any[][] = []
|
||||
|
||||
// Always include title and period
|
||||
worksheetData.push(['LAPORAN PENJUALAN KATEGORI'])
|
||||
worksheetData.push([`Periode: ${categoryData.date_from.split('T')[0]} - ${categoryData.date_to.split('T')[0]}`])
|
||||
worksheetData.push([])
|
||||
|
||||
// Optional summary
|
||||
if (options?.includeSummary !== false) {
|
||||
const categorySummary = {
|
||||
totalRevenue: categoryData.data?.reduce((sum, item) => sum + (item?.total_revenue || 0), 0) || 0,
|
||||
orderCount: categoryData.data?.reduce((sum, item) => sum + (item?.order_count || 0), 0) || 0,
|
||||
productCount: categoryData.data?.reduce((sum, item) => sum + (item?.product_count || 0), 0) || 0,
|
||||
totalQuantity: categoryData.data?.reduce((sum, item) => sum + (item?.total_quantity || 0), 0) || 0,
|
||||
totalCategories: categoryData.data?.length || 0
|
||||
}
|
||||
|
||||
worksheetData.push(['RINGKASAN PERIODE'])
|
||||
worksheetData.push([])
|
||||
const summaryData = [
|
||||
['Total Kategori:', categorySummary.totalCategories.toString()],
|
||||
['Total Produk:', categorySummary.productCount.toString()],
|
||||
['Total Quantity:', categorySummary.totalQuantity.toString()],
|
||||
['Total Revenue:', `Rp ${categorySummary.totalRevenue.toLocaleString('id-ID')}`]
|
||||
]
|
||||
summaryData.forEach(row => worksheetData.push([row[0], row[1]]))
|
||||
worksheetData.push([])
|
||||
worksheetData.push([])
|
||||
}
|
||||
|
||||
worksheetData.push(['RINCIAN KATEGORI'])
|
||||
worksheetData.push([])
|
||||
|
||||
// Header row based on options
|
||||
const headerRow =
|
||||
options?.includeOrderCount !== false
|
||||
? ['No', 'Nama', 'Total Produk', 'Qty', 'Total Orders', 'Pendapatan']
|
||||
: ['No', 'Nama', 'Total Produk', 'Qty', 'Pendapatan']
|
||||
worksheetData.push(headerRow)
|
||||
|
||||
// Add category data based on options
|
||||
categoryData.data?.forEach((category, index) => {
|
||||
const rowData =
|
||||
options?.includeOrderCount !== false
|
||||
? [
|
||||
index + 1,
|
||||
category.category_name,
|
||||
category.product_count,
|
||||
category.total_quantity,
|
||||
category.order_count,
|
||||
category.total_revenue
|
||||
]
|
||||
: [
|
||||
index + 1,
|
||||
category.category_name,
|
||||
category.product_count,
|
||||
category.total_quantity,
|
||||
category.total_revenue
|
||||
]
|
||||
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 || 'Penjualan Kategori'
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, sheetName)
|
||||
|
||||
const exportFilename = options?.customFilename || this.generateFilename('Custom_Category_Sales')
|
||||
XLSX.writeFile(workbook, exportFilename)
|
||||
|
||||
return { success: true, filename: exportFilename }
|
||||
} catch (error) {
|
||||
console.error('Error exporting custom category data to Excel:', error)
|
||||
return { success: false, error: 'Failed to export Excel file' }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,529 +0,0 @@
|
||||
// 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
|
||||
}
|
||||
}
|
||||
@ -1,556 +0,0 @@
|
||||
// services/pdfExportCategoryService.ts
|
||||
import { CategoryReport } from '@/types/services/analytic'
|
||||
|
||||
export class PDFExportSalesProductCategoryService {
|
||||
/**
|
||||
* Export Category Sales Report to PDF
|
||||
*/
|
||||
static async exportCategorySalesToPDF(categoryData: CategoryReport, 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.addCategoryReportContent(pdf, categoryData)
|
||||
|
||||
// Generate filename
|
||||
const exportFilename = filename || this.generateFilename('Laporan_Penjualan_Kategori', 'pdf')
|
||||
|
||||
// Save PDF
|
||||
pdf.save(exportFilename)
|
||||
|
||||
return { success: true, filename: exportFilename }
|
||||
} catch (error) {
|
||||
console.error('Error exporting category report to PDF:', error)
|
||||
return { success: false, error: `PDF export failed: ${(error as Error).message}` }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add category report content to PDF
|
||||
*/
|
||||
private static async addCategoryReportContent(pdf: any, categoryData: CategoryReport) {
|
||||
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, categoryData, yPos, pageWidth, marginLeft, marginRight)
|
||||
|
||||
// Section 1: Ringkasan
|
||||
checkPageBreak(50)
|
||||
yPos = this.addRingkasanSection(pdf, categoryData, yPos, pageWidth, marginLeft, marginRight, checkPageBreak)
|
||||
|
||||
// Section 2: Category Details
|
||||
checkPageBreak(80)
|
||||
yPos = this.addCategoryDetailsSection(pdf, categoryData, yPos, pageWidth, marginLeft, marginRight, checkPageBreak)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add report title
|
||||
*/
|
||||
private static addReportTitle(
|
||||
pdf: any,
|
||||
categoryData: CategoryReport,
|
||||
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 Penjualan Kategori', pageWidth / 2, yPos, { align: 'center' })
|
||||
yPos += 10
|
||||
|
||||
// Period
|
||||
pdf.setFontSize(12)
|
||||
pdf.setFont('helvetica', 'normal')
|
||||
const periodText = `${categoryData.date_from.split('T')[0]} - ${categoryData.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,
|
||||
categoryData: CategoryReport,
|
||||
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)
|
||||
|
||||
// Calculate summary
|
||||
const categorySummary = {
|
||||
totalRevenue: categoryData.data?.reduce((sum, item) => sum + (item?.total_revenue || 0), 0) || 0,
|
||||
orderCount: categoryData.data?.reduce((sum, item) => sum + (item?.order_count || 0), 0) || 0,
|
||||
productCount: categoryData.data?.reduce((sum, item) => sum + (item?.product_count || 0), 0) || 0,
|
||||
totalQuantity: categoryData.data?.reduce((sum, item) => sum + (item?.total_quantity || 0), 0) || 0,
|
||||
totalCategories: categoryData.data?.length || 0,
|
||||
averageRevenuePerCategory: 0
|
||||
}
|
||||
categorySummary.averageRevenuePerCategory =
|
||||
categorySummary.totalCategories > 0 ? categorySummary.totalRevenue / categorySummary.totalCategories : 0
|
||||
|
||||
const ringkasanItems = [
|
||||
{ label: 'Total Kategori', value: categorySummary.totalCategories.toString(), bold: false },
|
||||
{ label: 'Total Produk', value: categorySummary.productCount.toString(), bold: false },
|
||||
{ label: 'Total Quantity', value: categorySummary.totalQuantity.toString(), bold: false },
|
||||
{ label: 'Total Orders', value: categorySummary.orderCount.toString(), bold: false },
|
||||
{ label: 'Total Revenue', value: this.formatCurrency(categorySummary.totalRevenue), bold: true },
|
||||
{
|
||||
label: 'Rata-rata Revenue per Kategori',
|
||||
value: this.formatCurrency(categorySummary.averageRevenuePerCategory),
|
||||
bold: false
|
||||
}
|
||||
]
|
||||
|
||||
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 Category Details section
|
||||
*/
|
||||
private static addCategoryDetailsSection(
|
||||
pdf: any,
|
||||
categoryData: CategoryReport,
|
||||
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 Kategori', marginLeft, yPos)
|
||||
yPos += 12
|
||||
|
||||
// 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, tableWidth, 10, 'F')
|
||||
|
||||
pdf.setFont('helvetica', 'bold')
|
||||
pdf.setFontSize(9)
|
||||
|
||||
const headers = ['Nama', 'Total Produk', 'Qty', '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
|
||||
|
||||
// Calculate summary for footer
|
||||
const categorySummary = {
|
||||
totalRevenue: categoryData.data?.reduce((sum, item) => sum + (item?.total_revenue || 0), 0) || 0,
|
||||
productCount: categoryData.data?.reduce((sum, item) => sum + (item?.product_count || 0), 0) || 0,
|
||||
totalQuantity: categoryData.data?.reduce((sum, item) => sum + (item?.total_quantity || 0), 0) || 0
|
||||
}
|
||||
|
||||
// Table rows
|
||||
pdf.setFont('helvetica', 'normal')
|
||||
pdf.setFontSize(9)
|
||||
|
||||
categoryData.data?.forEach((category, index) => {
|
||||
if (checkPageBreak(10)) yPos = 20
|
||||
|
||||
currentX = marginLeft
|
||||
pdf.setFont('helvetica', 'normal')
|
||||
pdf.setFontSize(9)
|
||||
pdf.setTextColor(0, 0, 0)
|
||||
|
||||
// Category name
|
||||
pdf.text(category.category_name, currentX + 2, yPos + 5)
|
||||
currentX += colWidths[0]
|
||||
|
||||
// Product count
|
||||
pdf.text(category.product_count.toString(), currentX + colWidths[1] / 2, yPos + 5, { align: 'center' })
|
||||
currentX += colWidths[1]
|
||||
|
||||
// Quantity
|
||||
pdf.text(category.total_quantity.toString(), currentX + colWidths[2] / 2, yPos + 5, { align: 'center' })
|
||||
currentX += colWidths[2]
|
||||
|
||||
// Revenue
|
||||
pdf.text(this.formatCurrency(category.total_revenue), 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) // 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]
|
||||
|
||||
pdf.text(categorySummary.productCount.toString(), currentX + colWidths[1] / 2, yPos + 6, { align: 'center' })
|
||||
currentX += colWidths[1]
|
||||
|
||||
pdf.text(categorySummary.totalQuantity.toString(), currentX + colWidths[2] / 2, yPos + 6, { align: 'center' })
|
||||
currentX += colWidths[2]
|
||||
|
||||
pdf.text(this.formatCurrency(categorySummary.totalRevenue), currentX + colWidths[3] - 2, yPos + 6, {
|
||||
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}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Export Category Sales data with custom configuration
|
||||
*/
|
||||
static async exportCustomCategoryToPDF(
|
||||
categoryData: CategoryReport,
|
||||
options?: {
|
||||
title?: string
|
||||
includeSummary?: boolean
|
||||
customFilename?: string
|
||||
includeOrderCount?: 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, categoryData, yPos, pageWidth, marginLeft, marginRight)
|
||||
}
|
||||
|
||||
// Optional summary section
|
||||
if (options?.includeSummary !== false) {
|
||||
checkPageBreak(50)
|
||||
yPos = this.addRingkasanSection(pdf, categoryData, yPos, pageWidth, marginLeft, marginRight, checkPageBreak)
|
||||
}
|
||||
|
||||
// Category details
|
||||
checkPageBreak(80)
|
||||
yPos = this.addCategoryDetailsSection(pdf, categoryData, yPos, pageWidth, marginLeft, marginRight, checkPageBreak)
|
||||
|
||||
const exportFilename = options?.customFilename || this.generateFilename('Custom_Category_Sales', 'pdf')
|
||||
pdf.save(exportFilename)
|
||||
|
||||
return { success: true, filename: exportFilename }
|
||||
} catch (error) {
|
||||
console.error('Error exporting custom category report to PDF:', error)
|
||||
return { success: false, error: `PDF export failed: ${(error as Error).message}` }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export Category Sales with extended table including order count
|
||||
*/
|
||||
static async exportExtendedCategoryToPDF(categoryData: CategoryReport, filename?: 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
|
||||
}
|
||||
|
||||
// Title section
|
||||
yPos = this.addReportTitle(pdf, categoryData, yPos, pageWidth, marginLeft, marginRight)
|
||||
|
||||
// Summary section
|
||||
checkPageBreak(50)
|
||||
yPos = this.addRingkasanSection(pdf, categoryData, yPos, pageWidth, marginLeft, marginRight, checkPageBreak)
|
||||
|
||||
// Extended table with order count
|
||||
checkPageBreak(80)
|
||||
yPos = this.addExtendedCategoryDetailsSection(
|
||||
pdf,
|
||||
categoryData,
|
||||
yPos,
|
||||
pageWidth,
|
||||
marginLeft,
|
||||
marginRight,
|
||||
checkPageBreak
|
||||
)
|
||||
|
||||
const exportFilename = filename || this.generateFilename('Laporan_Kategori_Extended', 'pdf')
|
||||
pdf.save(exportFilename)
|
||||
|
||||
return { success: true, filename: exportFilename }
|
||||
} catch (error) {
|
||||
console.error('Error exporting extended category report to PDF:', error)
|
||||
return { success: false, error: `PDF export failed: ${(error as Error).message}` }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Extended Category Details section with order count
|
||||
*/
|
||||
private static addExtendedCategoryDetailsSection(
|
||||
pdf: any,
|
||||
categoryData: CategoryReport,
|
||||
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 Kategori (Extended)', marginLeft, yPos)
|
||||
yPos += 12
|
||||
|
||||
// Reset formatting
|
||||
pdf.setTextColor(0, 0, 0)
|
||||
|
||||
// Table setup - wider for 5 columns
|
||||
const tableWidth = pageWidth - marginLeft - marginRight
|
||||
const colWidths = [40, 25, 20, 20, 35] // Name, Products, Qty, Orders, Revenue
|
||||
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 = ['Nama', 'Total Produk', 'Qty', 'Orders', '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
|
||||
|
||||
// Calculate summary for footer
|
||||
const categorySummary = {
|
||||
totalRevenue: categoryData.data?.reduce((sum, item) => sum + (item?.total_revenue || 0), 0) || 0,
|
||||
productCount: categoryData.data?.reduce((sum, item) => sum + (item?.product_count || 0), 0) || 0,
|
||||
totalQuantity: categoryData.data?.reduce((sum, item) => sum + (item?.total_quantity || 0), 0) || 0,
|
||||
orderCount: categoryData.data?.reduce((sum, item) => sum + (item?.order_count || 0), 0) || 0
|
||||
}
|
||||
|
||||
// Table rows
|
||||
pdf.setFont('helvetica', 'normal')
|
||||
pdf.setFontSize(9)
|
||||
|
||||
categoryData.data?.forEach((category, index) => {
|
||||
if (checkPageBreak(10)) yPos = 20
|
||||
|
||||
currentX = marginLeft
|
||||
pdf.setFont('helvetica', 'normal')
|
||||
pdf.setFontSize(9)
|
||||
pdf.setTextColor(0, 0, 0)
|
||||
|
||||
// Category name
|
||||
const categoryName =
|
||||
category.category_name.length > 30 ? category.category_name.substring(0, 27) + '...' : category.category_name
|
||||
pdf.text(categoryName, currentX + 2, yPos + 5)
|
||||
currentX += colWidths[0]
|
||||
|
||||
// Product count
|
||||
pdf.text(category.product_count.toString(), currentX + colWidths[1] / 2, yPos + 5, { align: 'center' })
|
||||
currentX += colWidths[1]
|
||||
|
||||
// Quantity
|
||||
pdf.text(category.total_quantity.toString(), currentX + colWidths[2] / 2, yPos + 5, { align: 'center' })
|
||||
currentX += colWidths[2]
|
||||
|
||||
// Order count
|
||||
pdf.text(category.order_count.toString(), currentX + colWidths[3] / 2, yPos + 5, { align: 'center' })
|
||||
currentX += colWidths[3]
|
||||
|
||||
// Revenue
|
||||
pdf.text(this.formatCurrency(category.total_revenue), currentX + colWidths[4] - 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(9)
|
||||
|
||||
currentX = marginLeft
|
||||
pdf.text('TOTAL', currentX + 2, yPos + 6)
|
||||
currentX += colWidths[0]
|
||||
|
||||
pdf.text(categorySummary.productCount.toString(), currentX + colWidths[1] / 2, yPos + 6, { align: 'center' })
|
||||
currentX += colWidths[1]
|
||||
|
||||
pdf.text(categorySummary.totalQuantity.toString(), currentX + colWidths[2] / 2, yPos + 6, { align: 'center' })
|
||||
currentX += colWidths[2]
|
||||
|
||||
pdf.text(categorySummary.orderCount.toString(), currentX + colWidths[3] / 2, yPos + 6, { align: 'center' })
|
||||
currentX += colWidths[3]
|
||||
|
||||
pdf.text(this.formatCurrency(categorySummary.totalRevenue), currentX + colWidths[4] - 2, yPos + 6, {
|
||||
align: 'right'
|
||||
})
|
||||
|
||||
return yPos + 20
|
||||
}
|
||||
}
|
||||
@ -4,74 +4,19 @@ 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, Menu, MenuItem } from '@mui/material'
|
||||
import { Button, Card, CardContent } 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} />
|
||||
@ -83,30 +28,11 @@ const ReportSalesOrderContent = () => {
|
||||
color='secondary'
|
||||
variant='tonal'
|
||||
startIcon={<i className='tabler-upload' />}
|
||||
endIcon={<i className='tabler-chevron-down' />}
|
||||
className='max-sm:is-full'
|
||||
onClick={handleExportClick}
|
||||
// onClick={handleExportPDF}
|
||||
>
|
||||
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}
|
||||
|
||||
@ -2,17 +2,14 @@
|
||||
|
||||
import DateRangePicker from '@/components/RangeDatePicker'
|
||||
import { ReportItemHeader, ReportItemSubheader } from '@/components/report/ReportItem'
|
||||
import { ExcelExportSalesProductCategoryService } from '@/services/export/excel/ExcelExportSalesProductCategoryService'
|
||||
import { PDFExportSalesProductCategoryService } from '@/services/export/pdf/PDFExportSalesProductCategoryService'
|
||||
import { useCategoryAnalytics } from '@/services/queries/analytics'
|
||||
import { formatCurrency, formatDateDDMMYYYY } from '@/utils/transform'
|
||||
import { Button, Card, CardContent, Menu, MenuItem } from '@mui/material'
|
||||
import { Button, Card, CardContent } from '@mui/material'
|
||||
import { useState } from 'react'
|
||||
|
||||
const ReportSalesProductCategoryContent = () => {
|
||||
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: category } = useCategoryAnalytics({
|
||||
date_from: formatDateDDMMYYYY(startDate!),
|
||||
@ -26,58 +23,6 @@ const ReportSalesProductCategoryContent = () => {
|
||||
totalQuantity: category?.data?.reduce((sum, item) => sum + (item?.total_quantity || 0), 0) || 0
|
||||
}
|
||||
|
||||
const handleExportClick = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setAnchorEl(event.currentTarget)
|
||||
}
|
||||
|
||||
const handleExportClose = () => {
|
||||
setAnchorEl(null)
|
||||
}
|
||||
|
||||
const handleExportExcel = async () => {
|
||||
if (!category) {
|
||||
console.warn('No data available for export')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await ExcelExportSalesProductCategoryService.exportCategorySalesToExcel(
|
||||
category,
|
||||
`Penjualan_Kategori_${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 (!category) {
|
||||
console.warn('No data available for export')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await PDFExportSalesProductCategoryService.exportCategorySalesToPDF(
|
||||
category,
|
||||
`Laporan_Penjualan_Kategori_${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 (
|
||||
<Card>
|
||||
<div className='p-6 border-be'>
|
||||
@ -86,30 +31,11 @@ const ReportSalesProductCategoryContent = () => {
|
||||
color='secondary'
|
||||
variant='tonal'
|
||||
startIcon={<i className='tabler-upload' />}
|
||||
endIcon={<i className='tabler-chevron-down' />}
|
||||
className='max-sm:is-full'
|
||||
onClick={handleExportClick}
|
||||
// onClick={handleExportPDF}
|
||||
>
|
||||
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}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user