Compare commits
No commits in common. "69c49238d45b058af4792d484910b13fe0ab9264" and "52879b58fe3603164549d27e6e2375ed55919ea9" have entirely different histories.
69c49238d4
...
52879b58fe
@ -1,414 +0,0 @@
|
|||||||
// services/excelExportProductService.ts
|
|
||||||
import type { ProductSalesReport } from '@/types/services/analytic'
|
|
||||||
|
|
||||||
export class ExcelExportSalesProductService {
|
|
||||||
/**
|
|
||||||
* Export Product Sales Report to Excel
|
|
||||||
*/
|
|
||||||
static async exportProductSalesToExcel(productData: ProductSalesReport, 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 PRODUK']) // Row 0 - Main title
|
|
||||||
worksheetData.push([`Periode: ${productData.date_from.split('T')[0]} - ${productData.date_to.split('T')[0]}`]) // Row 1 - Period
|
|
||||||
worksheetData.push([]) // Empty row
|
|
||||||
|
|
||||||
// Calculate summary
|
|
||||||
const productSummary = {
|
|
||||||
totalQuantitySold: productData.data?.reduce((sum, item) => sum + (item?.quantity_sold || 0), 0) || 0,
|
|
||||||
totalRevenue: productData.data?.reduce((sum, item) => sum + (item?.revenue || 0), 0) || 0,
|
|
||||||
totalOrders: productData.data?.reduce((sum, item) => sum + (item?.order_count || 0), 0) || 0,
|
|
||||||
averageRevenue: 0
|
|
||||||
}
|
|
||||||
productSummary.averageRevenue =
|
|
||||||
productSummary.totalQuantitySold > 0 ? productSummary.totalRevenue / productSummary.totalQuantitySold : 0
|
|
||||||
|
|
||||||
// Add Summary Section
|
|
||||||
worksheetData.push(['RINGKASAN PERIODE']) // Section header
|
|
||||||
worksheetData.push([]) // Empty row
|
|
||||||
|
|
||||||
const summaryData = [
|
|
||||||
['Total Quantity Sold:', productSummary.totalQuantitySold.toString()],
|
|
||||||
['Total Orders:', productSummary.totalOrders.toString()],
|
|
||||||
['Total Revenue:', `Rp ${productSummary.totalRevenue.toLocaleString('id-ID')}`],
|
|
||||||
['Average Revenue per Item:', `Rp ${productSummary.averageRevenue.toLocaleString('id-ID')}`]
|
|
||||||
]
|
|
||||||
|
|
||||||
summaryData.forEach(row => {
|
|
||||||
worksheetData.push([row[0], row[1]]) // Only 2 columns needed
|
|
||||||
})
|
|
||||||
|
|
||||||
worksheetData.push([]) // Empty row
|
|
||||||
worksheetData.push([]) // Empty row
|
|
||||||
|
|
||||||
// Product Details Section Header
|
|
||||||
worksheetData.push(['RINCIAN PRODUK']) // Section header
|
|
||||||
worksheetData.push([]) // Empty row
|
|
||||||
|
|
||||||
// Group products by category
|
|
||||||
const groupedProducts =
|
|
||||||
productData.data?.reduce(
|
|
||||||
(acc, item) => {
|
|
||||||
const categoryName = item.category_name || 'Tidak Berkategori'
|
|
||||||
if (!acc[categoryName]) {
|
|
||||||
acc[categoryName] = []
|
|
||||||
}
|
|
||||||
acc[categoryName].push(item)
|
|
||||||
return acc
|
|
||||||
},
|
|
||||||
{} as Record<string, any[]>
|
|
||||||
) || {}
|
|
||||||
|
|
||||||
// Header row untuk tabel product data
|
|
||||||
const headerRow = ['Kategori', 'Produk', 'Qty', 'Order', 'Pendapatan', 'Rata Rata']
|
|
||||||
worksheetData.push(headerRow)
|
|
||||||
|
|
||||||
// Add grouped products data
|
|
||||||
Object.keys(groupedProducts)
|
|
||||||
.sort()
|
|
||||||
.forEach(categoryName => {
|
|
||||||
const categoryProducts = groupedProducts[categoryName]
|
|
||||||
|
|
||||||
// Category header row
|
|
||||||
worksheetData.push([categoryName.toUpperCase(), '', '', '', '', ''])
|
|
||||||
|
|
||||||
// Category products
|
|
||||||
categoryProducts.forEach(item => {
|
|
||||||
const rowData = [
|
|
||||||
'', // Empty for category column (indented effect)
|
|
||||||
item.product_name,
|
|
||||||
item.quantity_sold,
|
|
||||||
item.order_count || 0,
|
|
||||||
item.revenue, // Store as number for Excel formatting
|
|
||||||
item.average_price // Store as number for Excel formatting
|
|
||||||
]
|
|
||||||
worksheetData.push(rowData)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Category subtotal
|
|
||||||
const categoryTotalQty = categoryProducts.reduce((sum, item) => sum + (item.quantity_sold || 0), 0)
|
|
||||||
const categoryTotalOrders = categoryProducts.reduce((sum, item) => sum + (item.order_count || 0), 0)
|
|
||||||
const categoryTotalRevenue = categoryProducts.reduce((sum, item) => sum + (item.revenue || 0), 0)
|
|
||||||
|
|
||||||
const categorySubtotalRow = [
|
|
||||||
`Subtotal ${categoryName}`,
|
|
||||||
'',
|
|
||||||
categoryTotalQty,
|
|
||||||
categoryTotalOrders,
|
|
||||||
categoryTotalRevenue,
|
|
||||||
''
|
|
||||||
]
|
|
||||||
worksheetData.push(categorySubtotalRow)
|
|
||||||
worksheetData.push([]) // Empty row between categories
|
|
||||||
})
|
|
||||||
|
|
||||||
// Grand total
|
|
||||||
const grandTotalRow = [
|
|
||||||
'TOTAL KESELURUHAN',
|
|
||||||
'',
|
|
||||||
productSummary.totalQuantitySold,
|
|
||||||
productSummary.totalOrders,
|
|
||||||
productSummary.totalRevenue,
|
|
||||||
''
|
|
||||||
]
|
|
||||||
worksheetData.push(grandTotalRow)
|
|
||||||
|
|
||||||
// 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 Produk')
|
|
||||||
|
|
||||||
// Generate filename
|
|
||||||
const exportFilename = filename || this.generateFilename('Penjualan_Produk')
|
|
||||||
|
|
||||||
// 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: 25 }, // Kategori
|
|
||||||
{ wch: 40 }, // Produk
|
|
||||||
{ wch: 12 }, // Qty
|
|
||||||
{ wch: 12 }, // Order
|
|
||||||
{ wch: 20 }, // Pendapatan
|
|
||||||
{ wch: 18 } // Rata Rata
|
|
||||||
]
|
|
||||||
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 product 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 PRODUK') {
|
|
||||||
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 === 'Kategori') {
|
|
||||||
dataStartRow = i + 1
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dataStartRow === -1) return
|
|
||||||
|
|
||||||
// Apply currency formatting to Pendapatan and Rata Rata columns (columns 4 and 5)
|
|
||||||
for (let row = dataStartRow; row < totalRows; row++) {
|
|
||||||
// Pendapatan column (index 4)
|
|
||||||
const revenueCell = worksheet[XLSX.utils.encode_cell({ r: row, c: 4 })]
|
|
||||||
if (revenueCell && typeof revenueCell.v === 'number') {
|
|
||||||
revenueCell.z = '#,##0'
|
|
||||||
revenueCell.t = 'n'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rata Rata column (index 5)
|
|
||||||
const avgCell = worksheet[XLSX.utils.encode_cell({ r: row, c: 5 })]
|
|
||||||
if (avgCell && typeof avgCell.v === 'number') {
|
|
||||||
avgCell.z = '#,##0'
|
|
||||||
avgCell.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 category headers and totals
|
|
||||||
for (let row = dataStartRow; row < totalRows; row++) {
|
|
||||||
const cell = worksheet[XLSX.utils.encode_cell({ r: row, c: 0 })]
|
|
||||||
|
|
||||||
if (cell && cell.v) {
|
|
||||||
const cellValue = cell.v.toString()
|
|
||||||
|
|
||||||
// Style category headers (uppercase text without "Subtotal" or "TOTAL")
|
|
||||||
if (
|
|
||||||
cellValue === cellValue.toUpperCase() &&
|
|
||||||
!cellValue.includes('Subtotal') &&
|
|
||||||
!cellValue.includes('TOTAL') &&
|
|
||||||
cellValue.length > 0
|
|
||||||
) {
|
|
||||||
for (let col = 0; col < 6; col++) {
|
|
||||||
const categoryCellAddress = XLSX.utils.encode_cell({ r: row, c: col })
|
|
||||||
const categoryCell = worksheet[categoryCellAddress]
|
|
||||||
if (categoryCell) {
|
|
||||||
categoryCell.s = {
|
|
||||||
font: { bold: true, color: { rgb: '36175E' } },
|
|
||||||
fill: { fgColor: { rgb: 'F8F8F8' } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Style subtotal and total rows
|
|
||||||
if (cellValue.startsWith('Subtotal') || cellValue.startsWith('TOTAL')) {
|
|
||||||
for (let col = 0; col < 6; col++) {
|
|
||||||
const totalCellAddress = XLSX.utils.encode_cell({ r: row, c: col })
|
|
||||||
const totalCell = worksheet[totalCellAddress]
|
|
||||||
if (totalCell) {
|
|
||||||
totalCell.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 Product Sales data with custom configuration
|
|
||||||
*/
|
|
||||||
static async exportCustomProductData(
|
|
||||||
productData: ProductSalesReport,
|
|
||||||
options?: {
|
|
||||||
includeSummary?: boolean
|
|
||||||
customFilename?: string
|
|
||||||
sheetName?: string
|
|
||||||
groupByCategory?: boolean
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const XLSX = await import('xlsx')
|
|
||||||
const worksheetData: any[][] = []
|
|
||||||
|
|
||||||
// Always include title and period
|
|
||||||
worksheetData.push(['LAPORAN PENJUALAN PRODUK'])
|
|
||||||
worksheetData.push([`Periode: ${productData.date_from.split('T')[0]} - ${productData.date_to.split('T')[0]}`])
|
|
||||||
worksheetData.push([])
|
|
||||||
|
|
||||||
// Optional summary
|
|
||||||
if (options?.includeSummary !== false) {
|
|
||||||
const productSummary = {
|
|
||||||
totalQuantitySold: productData.data?.reduce((sum, item) => sum + (item?.quantity_sold || 0), 0) || 0,
|
|
||||||
totalRevenue: productData.data?.reduce((sum, item) => sum + (item?.revenue || 0), 0) || 0,
|
|
||||||
totalOrders: productData.data?.reduce((sum, item) => sum + (item?.order_count || 0), 0) || 0
|
|
||||||
}
|
|
||||||
|
|
||||||
worksheetData.push(['RINGKASAN PERIODE'])
|
|
||||||
worksheetData.push([])
|
|
||||||
const summaryData = [
|
|
||||||
['Total Quantity Sold:', productSummary.totalQuantitySold.toString()],
|
|
||||||
['Total Orders:', productSummary.totalOrders.toString()],
|
|
||||||
['Total Revenue:', `Rp ${productSummary.totalRevenue.toLocaleString('id-ID')}`]
|
|
||||||
]
|
|
||||||
summaryData.forEach(row => worksheetData.push([row[0], row[1]]))
|
|
||||||
worksheetData.push([])
|
|
||||||
worksheetData.push([])
|
|
||||||
}
|
|
||||||
|
|
||||||
worksheetData.push(['RINCIAN PRODUK'])
|
|
||||||
worksheetData.push([])
|
|
||||||
|
|
||||||
// Header row
|
|
||||||
const headerRow =
|
|
||||||
options?.groupByCategory !== false
|
|
||||||
? ['Kategori', 'Produk', 'Qty', 'Order', 'Pendapatan', 'Rata Rata']
|
|
||||||
: ['Produk', 'Kategori', 'Qty', 'Order', 'Pendapatan', 'Rata Rata']
|
|
||||||
worksheetData.push(headerRow)
|
|
||||||
|
|
||||||
// Add product data based on grouping option
|
|
||||||
if (options?.groupByCategory !== false) {
|
|
||||||
// Group by category (default)
|
|
||||||
const groupedProducts =
|
|
||||||
productData.data?.reduce(
|
|
||||||
(acc, item) => {
|
|
||||||
const categoryName = item.category_name || 'Tidak Berkategori'
|
|
||||||
if (!acc[categoryName]) {
|
|
||||||
acc[categoryName] = []
|
|
||||||
}
|
|
||||||
acc[categoryName].push(item)
|
|
||||||
return acc
|
|
||||||
},
|
|
||||||
{} as Record<string, any[]>
|
|
||||||
) || {}
|
|
||||||
|
|
||||||
Object.keys(groupedProducts)
|
|
||||||
.sort()
|
|
||||||
.forEach(categoryName => {
|
|
||||||
const categoryProducts = groupedProducts[categoryName]
|
|
||||||
worksheetData.push([categoryName.toUpperCase(), '', '', '', '', ''])
|
|
||||||
categoryProducts.forEach(item => {
|
|
||||||
worksheetData.push([
|
|
||||||
'',
|
|
||||||
item.product_name,
|
|
||||||
item.quantity_sold,
|
|
||||||
item.order_count || 0,
|
|
||||||
item.revenue,
|
|
||||||
item.average_price
|
|
||||||
])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// Flat list without grouping
|
|
||||||
productData.data?.forEach(item => {
|
|
||||||
worksheetData.push([
|
|
||||||
item.product_name,
|
|
||||||
item.category_name,
|
|
||||||
item.quantity_sold,
|
|
||||||
item.order_count || 0,
|
|
||||||
item.revenue,
|
|
||||||
item.average_price
|
|
||||||
])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
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 Produk'
|
|
||||||
XLSX.utils.book_append_sheet(workbook, worksheet, sheetName)
|
|
||||||
|
|
||||||
const exportFilename = options?.customFilename || this.generateFilename('Custom_Product_Sales')
|
|
||||||
XLSX.writeFile(workbook, exportFilename)
|
|
||||||
|
|
||||||
return { success: true, filename: exportFilename }
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error exporting custom product data to Excel:', error)
|
|
||||||
return { success: false, error: 'Failed to export Excel file' }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,469 +0,0 @@
|
|||||||
// services/excelExportSalesService.ts
|
|
||||||
import type { CategoryReport, PaymentReport, ProductSalesReport, ProfitLossReport } from '@/types/services/analytic'
|
|
||||||
|
|
||||||
export interface SalesReportData {
|
|
||||||
profitLoss: ProfitLossReport
|
|
||||||
paymentAnalytics: PaymentReport
|
|
||||||
categoryAnalytics: CategoryReport
|
|
||||||
productAnalytics: ProductSalesReport
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ExcelExportSalesService {
|
|
||||||
/**
|
|
||||||
* Export Sales Report to Excel
|
|
||||||
*/
|
|
||||||
static async exportSalesReportToExcel(salesData: SalesReportData, 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 TRANSAKSI']) // Row 0 - Main title
|
|
||||||
worksheetData.push([
|
|
||||||
`Periode: ${salesData.profitLoss.date_from.split('T')[0]} - ${salesData.profitLoss.date_to.split('T')[0]}`
|
|
||||||
]) // Row 1 - Period
|
|
||||||
worksheetData.push([]) // Empty row
|
|
||||||
|
|
||||||
// Add Summary Section (Ringkasan)
|
|
||||||
worksheetData.push(['RINGKASAN PERIODE']) // Section header
|
|
||||||
worksheetData.push([]) // Empty row
|
|
||||||
|
|
||||||
const ringkasanData = [
|
|
||||||
['Total Penjualan:', `Rp ${salesData.profitLoss.summary.total_revenue.toLocaleString('id-ID')}`],
|
|
||||||
['Total Diskon:', `Rp ${salesData.profitLoss.summary.total_discount.toLocaleString('id-ID')}`],
|
|
||||||
['Total Pajak:', `Rp ${salesData.profitLoss.summary.total_tax.toLocaleString('id-ID')}`],
|
|
||||||
['Total:', `Rp ${salesData.profitLoss.summary.total_revenue.toLocaleString('id-ID')}`]
|
|
||||||
]
|
|
||||||
|
|
||||||
ringkasanData.forEach(row => {
|
|
||||||
worksheetData.push([row[0], row[1]])
|
|
||||||
})
|
|
||||||
|
|
||||||
worksheetData.push([]) // Empty row
|
|
||||||
worksheetData.push([]) // Empty row
|
|
||||||
|
|
||||||
// Add Invoice Section
|
|
||||||
worksheetData.push(['INVOICE']) // Section header
|
|
||||||
worksheetData.push([]) // Empty row
|
|
||||||
|
|
||||||
const invoiceData = [
|
|
||||||
['Total Invoice:', salesData.profitLoss.summary.total_orders.toString()],
|
|
||||||
['Rata-rata Tagihan Per Invoice:', `Rp ${salesData.profitLoss.summary.average_profit.toLocaleString('id-ID')}`]
|
|
||||||
]
|
|
||||||
|
|
||||||
invoiceData.forEach(row => {
|
|
||||||
worksheetData.push([row[0], row[1]])
|
|
||||||
})
|
|
||||||
|
|
||||||
worksheetData.push([]) // Empty row
|
|
||||||
worksheetData.push([]) // Empty row
|
|
||||||
|
|
||||||
// Add Payment Methods Section
|
|
||||||
worksheetData.push(['RINGKASAN METODE PEMBAYARAN']) // Section header
|
|
||||||
worksheetData.push([]) // Empty row
|
|
||||||
|
|
||||||
// Payment methods table header
|
|
||||||
const paymentHeaderRow = ['No', 'Metode Pembayaran', 'Tipe', 'Jumlah Order', 'Total Amount', 'Persentase']
|
|
||||||
worksheetData.push(paymentHeaderRow)
|
|
||||||
|
|
||||||
// Payment methods data
|
|
||||||
salesData.paymentAnalytics.data?.forEach((payment, index) => {
|
|
||||||
const rowData = [
|
|
||||||
index + 1,
|
|
||||||
payment.payment_method_name,
|
|
||||||
payment.payment_method_type.toUpperCase(),
|
|
||||||
payment.order_count,
|
|
||||||
payment.total_amount,
|
|
||||||
`${(payment.percentage ?? 0).toFixed(1)}%`
|
|
||||||
]
|
|
||||||
worksheetData.push(rowData)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Payment methods total row
|
|
||||||
const paymentTotalRow = [
|
|
||||||
'TOTAL',
|
|
||||||
'',
|
|
||||||
'',
|
|
||||||
salesData.paymentAnalytics.summary?.total_orders ?? 0,
|
|
||||||
salesData.paymentAnalytics.summary?.total_amount ?? 0,
|
|
||||||
'100.0%'
|
|
||||||
]
|
|
||||||
worksheetData.push(paymentTotalRow)
|
|
||||||
|
|
||||||
worksheetData.push([]) // Empty row
|
|
||||||
worksheetData.push([]) // Empty row
|
|
||||||
|
|
||||||
// Add Category Section
|
|
||||||
worksheetData.push(['RINGKASAN KATEGORI']) // Section header
|
|
||||||
worksheetData.push([]) // Empty row
|
|
||||||
|
|
||||||
// Category table header
|
|
||||||
const categoryHeaderRow = ['No', 'Nama', 'Total Produk', 'Qty', 'Pendapatan']
|
|
||||||
worksheetData.push(categoryHeaderRow)
|
|
||||||
|
|
||||||
// Calculate category summaries
|
|
||||||
const categorySummary = {
|
|
||||||
totalRevenue: salesData.categoryAnalytics.data?.reduce((sum, item) => sum + (item?.total_revenue || 0), 0) || 0,
|
|
||||||
productCount: salesData.categoryAnalytics.data?.reduce((sum, item) => sum + (item?.product_count || 0), 0) || 0,
|
|
||||||
totalQuantity:
|
|
||||||
salesData.categoryAnalytics.data?.reduce((sum, item) => sum + (item?.total_quantity || 0), 0) || 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Category data
|
|
||||||
salesData.categoryAnalytics.data?.forEach((category, index) => {
|
|
||||||
const rowData = [
|
|
||||||
index + 1,
|
|
||||||
category.category_name,
|
|
||||||
category.product_count,
|
|
||||||
category.total_quantity,
|
|
||||||
category.total_revenue
|
|
||||||
]
|
|
||||||
worksheetData.push(rowData)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Category total row
|
|
||||||
const categoryTotalRow = [
|
|
||||||
'TOTAL',
|
|
||||||
'',
|
|
||||||
categorySummary.productCount,
|
|
||||||
categorySummary.totalQuantity,
|
|
||||||
categorySummary.totalRevenue
|
|
||||||
]
|
|
||||||
worksheetData.push(categoryTotalRow)
|
|
||||||
|
|
||||||
worksheetData.push([]) // Empty row
|
|
||||||
worksheetData.push([]) // Empty row
|
|
||||||
|
|
||||||
// Add Product Section
|
|
||||||
worksheetData.push(['RINGKASAN ITEM']) // Section header
|
|
||||||
worksheetData.push([]) // Empty row
|
|
||||||
|
|
||||||
// Group products by category
|
|
||||||
const groupedProducts =
|
|
||||||
salesData.productAnalytics.data?.reduce(
|
|
||||||
(acc, item) => {
|
|
||||||
const categoryName = item.category_name || 'Tidak Berkategori'
|
|
||||||
if (!acc[categoryName]) {
|
|
||||||
acc[categoryName] = []
|
|
||||||
}
|
|
||||||
acc[categoryName].push(item)
|
|
||||||
return acc
|
|
||||||
},
|
|
||||||
{} as Record<string, any[]>
|
|
||||||
) || {}
|
|
||||||
|
|
||||||
// Calculate product summary
|
|
||||||
const productSummary = {
|
|
||||||
totalQuantitySold:
|
|
||||||
salesData.productAnalytics.data?.reduce((sum, item) => sum + (item?.quantity_sold || 0), 0) || 0,
|
|
||||||
totalRevenue: salesData.productAnalytics.data?.reduce((sum, item) => sum + (item?.revenue || 0), 0) || 0,
|
|
||||||
totalOrders: salesData.productAnalytics.data?.reduce((sum, item) => sum + (item?.order_count || 0), 0) || 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Product table header
|
|
||||||
const productHeaderRow = ['Kategori', 'Produk', 'Qty', 'Order', 'Pendapatan', 'Rata Rata']
|
|
||||||
worksheetData.push(productHeaderRow)
|
|
||||||
|
|
||||||
// Add grouped products data
|
|
||||||
Object.keys(groupedProducts)
|
|
||||||
.sort()
|
|
||||||
.forEach(categoryName => {
|
|
||||||
const categoryProducts = groupedProducts[categoryName]
|
|
||||||
|
|
||||||
// Category header row
|
|
||||||
worksheetData.push([categoryName.toUpperCase(), '', '', '', '', ''])
|
|
||||||
|
|
||||||
// Category products
|
|
||||||
categoryProducts.forEach(item => {
|
|
||||||
const rowData = [
|
|
||||||
'',
|
|
||||||
item.product_name,
|
|
||||||
item.quantity_sold,
|
|
||||||
item.order_count || 0,
|
|
||||||
item.revenue,
|
|
||||||
item.average_price
|
|
||||||
]
|
|
||||||
worksheetData.push(rowData)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Category subtotal
|
|
||||||
const categoryTotalQty = categoryProducts.reduce((sum, item) => sum + (item.quantity_sold || 0), 0)
|
|
||||||
const categoryTotalOrders = categoryProducts.reduce((sum, item) => sum + (item.order_count || 0), 0)
|
|
||||||
const categoryTotalRevenue = categoryProducts.reduce((sum, item) => sum + (item.revenue || 0), 0)
|
|
||||||
const categoryAverage = categoryTotalQty > 0 ? categoryTotalRevenue / categoryTotalQty : 0
|
|
||||||
|
|
||||||
const categorySubtotalRow = [
|
|
||||||
`Subtotal ${categoryName}`,
|
|
||||||
'',
|
|
||||||
categoryTotalQty,
|
|
||||||
categoryTotalOrders,
|
|
||||||
categoryTotalRevenue,
|
|
||||||
categoryAverage
|
|
||||||
]
|
|
||||||
worksheetData.push(categorySubtotalRow)
|
|
||||||
worksheetData.push([]) // Empty row between categories
|
|
||||||
})
|
|
||||||
|
|
||||||
// Grand total
|
|
||||||
const grandTotalAverage =
|
|
||||||
productSummary.totalQuantitySold > 0 ? productSummary.totalRevenue / productSummary.totalQuantitySold : 0
|
|
||||||
const grandTotalRow = [
|
|
||||||
'TOTAL KESELURUHAN',
|
|
||||||
'',
|
|
||||||
productSummary.totalQuantitySold,
|
|
||||||
productSummary.totalOrders,
|
|
||||||
productSummary.totalRevenue,
|
|
||||||
grandTotalAverage
|
|
||||||
]
|
|
||||||
worksheetData.push(grandTotalRow)
|
|
||||||
|
|
||||||
// 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, 'Laporan Transaksi')
|
|
||||||
|
|
||||||
// Generate filename
|
|
||||||
const exportFilename = filename || this.generateFilename('Laporan_Transaksi')
|
|
||||||
|
|
||||||
// Download file
|
|
||||||
XLSX.writeFile(workbook, exportFilename)
|
|
||||||
|
|
||||||
return { success: true, filename: exportFilename }
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error exporting sales report 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: 25 }, // First column (category/label)
|
|
||||||
{ wch: 30 }, // Second column (description/name)
|
|
||||||
{ wch: 15 }, // Third column (numbers)
|
|
||||||
{ wch: 15 }, // Fourth column (numbers)
|
|
||||||
{ wch: 20 }, // Fifth column (amounts)
|
|
||||||
{ wch: 15 } // Sixth column (percentages/averages)
|
|
||||||
]
|
|
||||||
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 }, // Section headers
|
|
||||||
{ hpt: 15 } // Empty row
|
|
||||||
]
|
|
||||||
|
|
||||||
// Merge cells untuk main headers
|
|
||||||
const merges = [
|
|
||||||
{ s: { r: 0, c: 0 }, e: { r: 0, c: 5 } }, // Main title
|
|
||||||
{ s: { r: 1, c: 0 }, e: { r: 1, c: 5 } } // Period
|
|
||||||
]
|
|
||||||
|
|
||||||
// Find and add merges for section headers
|
|
||||||
const sectionHeaders = [
|
|
||||||
'RINGKASAN PERIODE',
|
|
||||||
'INVOICE',
|
|
||||||
'RINGKASAN METODE PEMBAYARAN',
|
|
||||||
'RINGKASAN KATEGORI',
|
|
||||||
'RINGKASAN ITEM'
|
|
||||||
]
|
|
||||||
|
|
||||||
for (let i = 0; i < totalRows; i++) {
|
|
||||||
const cell = worksheet[XLSX.utils.encode_cell({ r: i, c: 0 })]
|
|
||||||
if (cell && sectionHeaders.includes(cell.v)) {
|
|
||||||
merges.push({ s: { r: i, c: 0 }, e: { r: i, c: 5 } })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
worksheet['!merges'] = merges
|
|
||||||
|
|
||||||
// Apply number formatting untuk currency cells
|
|
||||||
this.applyNumberFormatting(worksheet, totalRows, XLSX)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Apply number formatting for currency
|
|
||||||
*/
|
|
||||||
private static applyNumberFormatting(worksheet: any, totalRows: number, XLSX: any) {
|
|
||||||
// Apply currency formatting to amount columns
|
|
||||||
for (let row = 0; row < totalRows; row++) {
|
|
||||||
// Check columns that might contain currency values (columns 1, 4, 5)
|
|
||||||
;[1, 4, 5].forEach(col => {
|
|
||||||
const cellAddress = XLSX.utils.encode_cell({ r: row, c: col })
|
|
||||||
const cell = worksheet[cellAddress]
|
|
||||||
|
|
||||||
if (cell && typeof cell.v === 'number' && cell.v > 1000) {
|
|
||||||
// Apply Indonesian currency format for large numbers
|
|
||||||
cell.z = '#,##0'
|
|
||||||
cell.t = 'n'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply formatting to specific sections
|
|
||||||
this.applySectionFormatting(worksheet, totalRows, XLSX)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Apply specific formatting to sections
|
|
||||||
*/
|
|
||||||
private static applySectionFormatting(worksheet: any, totalRows: number, XLSX: any) {
|
|
||||||
// Find and format table headers and total rows
|
|
||||||
const headerKeywords = ['No', 'Metode Pembayaran', 'Nama', 'Kategori', 'Produk']
|
|
||||||
const totalKeywords = ['TOTAL', 'Subtotal']
|
|
||||||
|
|
||||||
for (let row = 0; row < totalRows; row++) {
|
|
||||||
const cell = worksheet[XLSX.utils.encode_cell({ r: row, c: 0 })]
|
|
||||||
|
|
||||||
if (cell) {
|
|
||||||
// Format table headers
|
|
||||||
if (headerKeywords.some(keyword => cell.v === keyword)) {
|
|
||||||
for (let col = 0; col < 6; col++) {
|
|
||||||
const headerCellAddress = XLSX.utils.encode_cell({ r: row, c: col })
|
|
||||||
const headerCell = worksheet[headerCellAddress]
|
|
||||||
if (headerCell) {
|
|
||||||
headerCell.s = {
|
|
||||||
font: { bold: true },
|
|
||||||
fill: { fgColor: { rgb: 'F3F4F6' } },
|
|
||||||
border: {
|
|
||||||
bottom: { style: 'medium', color: { rgb: '000000' } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format total rows
|
|
||||||
if (totalKeywords.some(keyword => cell.v?.toString().startsWith(keyword))) {
|
|
||||||
for (let col = 0; col < 6; col++) {
|
|
||||||
const totalCellAddress = XLSX.utils.encode_cell({ r: row, c: col })
|
|
||||||
const totalCell = worksheet[totalCellAddress]
|
|
||||||
if (totalCell) {
|
|
||||||
totalCell.s = {
|
|
||||||
font: { bold: true },
|
|
||||||
border: {
|
|
||||||
top: { style: 'medium', color: { rgb: '000000' } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format section headers
|
|
||||||
const sectionHeaders = [
|
|
||||||
'RINGKASAN PERIODE',
|
|
||||||
'INVOICE',
|
|
||||||
'RINGKASAN METODE PEMBAYARAN',
|
|
||||||
'RINGKASAN KATEGORI',
|
|
||||||
'RINGKASAN ITEM'
|
|
||||||
]
|
|
||||||
if (sectionHeaders.includes(cell.v)) {
|
|
||||||
cell.s = {
|
|
||||||
font: { bold: true, color: { rgb: '662D91' } },
|
|
||||||
fill: { fgColor: { rgb: 'F8F9FA' } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 custom sales data to Excel with configuration
|
|
||||||
*/
|
|
||||||
static async exportCustomSalesData(
|
|
||||||
salesData: SalesReportData,
|
|
||||||
options?: {
|
|
||||||
includeSections?: {
|
|
||||||
ringkasan?: boolean
|
|
||||||
invoice?: boolean
|
|
||||||
paymentMethods?: boolean
|
|
||||||
categories?: boolean
|
|
||||||
products?: boolean
|
|
||||||
}
|
|
||||||
customFilename?: string
|
|
||||||
sheetName?: string
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const XLSX = await import('xlsx')
|
|
||||||
const worksheetData: any[][] = []
|
|
||||||
|
|
||||||
// Always include title and period
|
|
||||||
worksheetData.push(['LAPORAN TRANSAKSI'])
|
|
||||||
worksheetData.push([
|
|
||||||
`Periode: ${salesData.profitLoss.date_from.split('T')[0]} - ${salesData.profitLoss.date_to.split('T')[0]}`
|
|
||||||
])
|
|
||||||
worksheetData.push([])
|
|
||||||
|
|
||||||
const sections = options?.includeSections || {
|
|
||||||
ringkasan: true,
|
|
||||||
invoice: true,
|
|
||||||
paymentMethods: true,
|
|
||||||
categories: true,
|
|
||||||
products: true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Conditionally add sections based on options
|
|
||||||
if (sections.ringkasan) {
|
|
||||||
worksheetData.push(['RINGKASAN PERIODE'])
|
|
||||||
worksheetData.push([])
|
|
||||||
// Add ringkasan data...
|
|
||||||
const ringkasanData = [
|
|
||||||
['Total Penjualan:', `Rp ${salesData.profitLoss.summary.total_revenue.toLocaleString('id-ID')}`],
|
|
||||||
['Total Diskon:', `Rp ${salesData.profitLoss.summary.total_discount.toLocaleString('id-ID')}`],
|
|
||||||
['Total Pajak:', `Rp ${salesData.profitLoss.summary.total_tax.toLocaleString('id-ID')}`],
|
|
||||||
['Total:', `Rp ${salesData.profitLoss.summary.total_revenue.toLocaleString('id-ID')}`]
|
|
||||||
]
|
|
||||||
ringkasanData.forEach(row => worksheetData.push([row[0], row[1]]))
|
|
||||||
worksheetData.push([])
|
|
||||||
worksheetData.push([])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add other sections similarly based on options...
|
|
||||||
|
|
||||||
const workbook = XLSX.utils.book_new()
|
|
||||||
const worksheet = XLSX.utils.aoa_to_sheet(worksheetData)
|
|
||||||
|
|
||||||
this.applyBasicFormatting(worksheet, worksheetData.length, XLSX)
|
|
||||||
|
|
||||||
const sheetName = options?.sheetName || 'Laporan Transaksi'
|
|
||||||
XLSX.utils.book_append_sheet(workbook, worksheet, sheetName)
|
|
||||||
|
|
||||||
const exportFilename = options?.customFilename || this.generateFilename('Custom_Sales_Report')
|
|
||||||
XLSX.writeFile(workbook, exportFilename)
|
|
||||||
|
|
||||||
return { success: true, filename: exportFilename }
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error exporting custom sales report to Excel:', error)
|
|
||||||
return { success: false, error: 'Failed to export Excel file' }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,364 +0,0 @@
|
|||||||
// services/pdfExportPaymentService.ts
|
|
||||||
import { PaymentReport } from '@/types/services/analytic'
|
|
||||||
|
|
||||||
export class PDFExportPaymentService {
|
|
||||||
/**
|
|
||||||
* Export Payment Method Report to PDF
|
|
||||||
*/
|
|
||||||
static async exportPaymentMethodToPDF(paymentData: PaymentReport, filename?: string) {
|
|
||||||
try {
|
|
||||||
// Dynamic import untuk jsPDF
|
|
||||||
const jsPDFModule = await import('jspdf')
|
|
||||||
const jsPDF = jsPDFModule.default
|
|
||||||
|
|
||||||
// Create new PDF document - PORTRAIT A4
|
|
||||||
const pdf = new jsPDF('p', 'mm', 'a4')
|
|
||||||
|
|
||||||
// Add content
|
|
||||||
await this.addPaymentReportContent(pdf, paymentData)
|
|
||||||
|
|
||||||
// Generate filename
|
|
||||||
const exportFilename = filename || this.generateFilename('Laporan_Metode_Pembayaran', 'pdf')
|
|
||||||
|
|
||||||
// Save PDF
|
|
||||||
pdf.save(exportFilename)
|
|
||||||
|
|
||||||
return { success: true, filename: exportFilename }
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error exporting payment report to PDF:', error)
|
|
||||||
return { success: false, error: `PDF export failed: ${(error as Error).message}` }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add payment report content to PDF
|
|
||||||
*/
|
|
||||||
private static async addPaymentReportContent(pdf: any, paymentData: PaymentReport) {
|
|
||||||
let yPos = 20
|
|
||||||
const pageWidth = pdf.internal.pageSize.getWidth()
|
|
||||||
const pageHeight = pdf.internal.pageSize.getHeight()
|
|
||||||
const marginLeft = 20
|
|
||||||
const marginRight = 20
|
|
||||||
const marginBottom = 15
|
|
||||||
|
|
||||||
// Helper function to check page break
|
|
||||||
const checkPageBreak = (neededSpace: number) => {
|
|
||||||
if (yPos + neededSpace > pageHeight - marginBottom) {
|
|
||||||
pdf.addPage()
|
|
||||||
yPos = 20
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Title
|
|
||||||
yPos = this.addReportTitle(pdf, paymentData, yPos, pageWidth, marginLeft, marginRight)
|
|
||||||
|
|
||||||
// Section 1: Ringkasan
|
|
||||||
checkPageBreak(50)
|
|
||||||
yPos = this.addRingkasanSection(pdf, paymentData, yPos, pageWidth, marginLeft, marginRight, checkPageBreak)
|
|
||||||
|
|
||||||
// Section 2: Payment Methods Detail
|
|
||||||
checkPageBreak(80)
|
|
||||||
yPos = this.addPaymentMethodsSection(pdf, paymentData, yPos, pageWidth, marginLeft, marginRight, checkPageBreak)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add report title
|
|
||||||
*/
|
|
||||||
private static addReportTitle(
|
|
||||||
pdf: any,
|
|
||||||
paymentData: PaymentReport,
|
|
||||||
startY: number,
|
|
||||||
pageWidth: number,
|
|
||||||
marginLeft: number,
|
|
||||||
marginRight: number
|
|
||||||
): number {
|
|
||||||
let yPos = startY
|
|
||||||
|
|
||||||
// Title
|
|
||||||
pdf.setFontSize(20)
|
|
||||||
pdf.setFont('helvetica', 'bold')
|
|
||||||
pdf.setTextColor(0, 0, 0)
|
|
||||||
pdf.text('Laporan Metode Pembayaran', pageWidth / 2, yPos, { align: 'center' })
|
|
||||||
yPos += 10
|
|
||||||
|
|
||||||
// Period
|
|
||||||
pdf.setFontSize(12)
|
|
||||||
pdf.setFont('helvetica', 'normal')
|
|
||||||
const periodText = `${paymentData.date_from.split('T')[0]} - ${paymentData.date_to.split('T')[0]}`
|
|
||||||
pdf.text(periodText, pageWidth / 2, yPos, { align: 'center' })
|
|
||||||
yPos += 10
|
|
||||||
|
|
||||||
// Purple line separator
|
|
||||||
pdf.setDrawColor(102, 45, 145)
|
|
||||||
pdf.setLineWidth(2)
|
|
||||||
pdf.line(marginLeft, yPos, pageWidth - marginRight, yPos)
|
|
||||||
yPos += 15
|
|
||||||
|
|
||||||
return yPos
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add Ringkasan section - SAMA SEPERTI SALES REPORT STYLE
|
|
||||||
*/
|
|
||||||
private static addRingkasanSection(
|
|
||||||
pdf: any,
|
|
||||||
paymentData: PaymentReport,
|
|
||||||
startY: number,
|
|
||||||
pageWidth: number,
|
|
||||||
marginLeft: number,
|
|
||||||
marginRight: number,
|
|
||||||
checkPageBreak: (space: number) => boolean
|
|
||||||
): number {
|
|
||||||
let yPos = startY
|
|
||||||
|
|
||||||
// Section title - SAMA SEPERTI SALES REPORT
|
|
||||||
pdf.setFontSize(14)
|
|
||||||
pdf.setFont('helvetica', 'bold')
|
|
||||||
pdf.setTextColor(102, 45, 145)
|
|
||||||
pdf.text('Ringkasan', marginLeft, yPos)
|
|
||||||
yPos += 12
|
|
||||||
|
|
||||||
// Reset text color
|
|
||||||
pdf.setTextColor(0, 0, 0)
|
|
||||||
pdf.setFontSize(11)
|
|
||||||
|
|
||||||
const ringkasanItems = [
|
|
||||||
{ label: 'Total Amount', value: this.formatCurrency(paymentData.summary.total_amount), bold: false },
|
|
||||||
{ label: 'Total Orders', value: paymentData.summary.total_orders.toString(), bold: false },
|
|
||||||
{ label: 'Total Payments', value: paymentData.summary.total_payments.toString(), bold: false },
|
|
||||||
{ label: 'Average Order Value', value: this.formatCurrency(paymentData.summary.average_order_value), bold: true }
|
|
||||||
]
|
|
||||||
|
|
||||||
ringkasanItems.forEach((item, index) => {
|
|
||||||
if (checkPageBreak(15)) yPos = 20
|
|
||||||
|
|
||||||
// Set font weight
|
|
||||||
pdf.setFont('helvetica', item.bold ? 'bold' : 'normal')
|
|
||||||
|
|
||||||
pdf.text(item.label, marginLeft, yPos)
|
|
||||||
pdf.text(item.value, pageWidth - marginRight, yPos, { align: 'right' })
|
|
||||||
|
|
||||||
// Light separator line (except for last row)
|
|
||||||
if (!item.bold) {
|
|
||||||
pdf.setDrawColor(230, 230, 230)
|
|
||||||
pdf.setLineWidth(0.5)
|
|
||||||
pdf.line(marginLeft, yPos + 2, pageWidth - marginRight, yPos + 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
yPos += 8
|
|
||||||
})
|
|
||||||
|
|
||||||
return yPos + 20
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add Payment Methods section - ORIGINAL STYLE DARI SALES REPORT
|
|
||||||
*/
|
|
||||||
private static addPaymentMethodsSection(
|
|
||||||
pdf: any,
|
|
||||||
paymentData: PaymentReport,
|
|
||||||
startY: number,
|
|
||||||
pageWidth: number,
|
|
||||||
marginLeft: number,
|
|
||||||
marginRight: number,
|
|
||||||
checkPageBreak: (space: number) => boolean
|
|
||||||
): number {
|
|
||||||
let yPos = startY
|
|
||||||
|
|
||||||
// Section title
|
|
||||||
pdf.setFontSize(14)
|
|
||||||
pdf.setFont('helvetica', 'bold')
|
|
||||||
pdf.setTextColor(102, 45, 145)
|
|
||||||
pdf.text('Rincian Metode Pembayaran', marginLeft, yPos)
|
|
||||||
yPos += 12
|
|
||||||
|
|
||||||
// Reset formatting
|
|
||||||
pdf.setTextColor(0, 0, 0)
|
|
||||||
|
|
||||||
// Table setup
|
|
||||||
const tableWidth = pageWidth - marginLeft - marginRight
|
|
||||||
const colWidths = [50, 25, 30, 35, 25] // Method, Type, Order, Amount, %
|
|
||||||
let currentX = marginLeft
|
|
||||||
|
|
||||||
// Table header
|
|
||||||
pdf.setFillColor(240, 240, 240)
|
|
||||||
pdf.rect(marginLeft, yPos, tableWidth, 10, 'F')
|
|
||||||
|
|
||||||
pdf.setFont('helvetica', 'bold')
|
|
||||||
pdf.setFontSize(9)
|
|
||||||
|
|
||||||
const headers = ['Metode Pembayaran', 'Tipe', 'Jumlah Order', 'Total Amount', 'Persentase']
|
|
||||||
currentX = marginLeft
|
|
||||||
|
|
||||||
headers.forEach((header, index) => {
|
|
||||||
if (index === 0) {
|
|
||||||
pdf.text(header, currentX + 2, yPos + 6)
|
|
||||||
} else {
|
|
||||||
pdf.text(header, currentX + colWidths[index] / 2, yPos + 6, { align: 'center' })
|
|
||||||
}
|
|
||||||
currentX += colWidths[index]
|
|
||||||
})
|
|
||||||
|
|
||||||
yPos += 12
|
|
||||||
|
|
||||||
// Table rows
|
|
||||||
pdf.setFont('helvetica', 'normal')
|
|
||||||
pdf.setFontSize(9)
|
|
||||||
|
|
||||||
paymentData.data?.forEach((payment, index) => {
|
|
||||||
if (checkPageBreak(10)) yPos = 20
|
|
||||||
|
|
||||||
currentX = marginLeft
|
|
||||||
pdf.setFont('helvetica', 'normal')
|
|
||||||
pdf.setFontSize(9)
|
|
||||||
pdf.setTextColor(0, 0, 0)
|
|
||||||
|
|
||||||
// Method name
|
|
||||||
pdf.text(payment.payment_method_name, currentX + 2, yPos + 5)
|
|
||||||
currentX += colWidths[0]
|
|
||||||
|
|
||||||
// Type with simple color coding
|
|
||||||
const typeText = payment.payment_method_type.toUpperCase()
|
|
||||||
if (payment.payment_method_type === 'cash') {
|
|
||||||
pdf.setTextColor(0, 120, 0) // Green
|
|
||||||
} else if (payment.payment_method_type === 'card') {
|
|
||||||
pdf.setTextColor(0, 80, 200) // Blue
|
|
||||||
} else {
|
|
||||||
pdf.setTextColor(200, 100, 0) // Orange
|
|
||||||
}
|
|
||||||
pdf.text(typeText, currentX + colWidths[1] / 2, yPos + 5, { align: 'center' })
|
|
||||||
pdf.setTextColor(0, 0, 0) // Reset color
|
|
||||||
currentX += colWidths[1]
|
|
||||||
|
|
||||||
// Order count
|
|
||||||
pdf.text(payment.order_count.toString(), currentX + colWidths[2] / 2, yPos + 5, { align: 'center' })
|
|
||||||
currentX += colWidths[2]
|
|
||||||
|
|
||||||
// Amount
|
|
||||||
pdf.text(this.formatCurrency(payment.total_amount), currentX + colWidths[3] - 2, yPos + 5, { align: 'right' })
|
|
||||||
currentX += colWidths[3]
|
|
||||||
|
|
||||||
// Percentage
|
|
||||||
pdf.text(`${(payment.percentage ?? 0).toFixed(1)}%`, currentX + colWidths[4] / 2, yPos + 5, { align: 'center' })
|
|
||||||
|
|
||||||
// Draw bottom border line
|
|
||||||
pdf.setDrawColor(230, 230, 230)
|
|
||||||
pdf.setLineWidth(0.3)
|
|
||||||
pdf.line(marginLeft, yPos + 8, marginLeft + tableWidth, yPos + 8)
|
|
||||||
|
|
||||||
yPos += 10
|
|
||||||
})
|
|
||||||
|
|
||||||
// Table footer (Total) - directly after last row
|
|
||||||
pdf.setFillColor(245, 245, 245) // Lighter gray
|
|
||||||
pdf.rect(marginLeft, yPos, tableWidth, 10, 'F')
|
|
||||||
|
|
||||||
pdf.setFont('helvetica', 'bold')
|
|
||||||
pdf.setFontSize(9)
|
|
||||||
|
|
||||||
currentX = marginLeft
|
|
||||||
pdf.text('TOTAL', currentX + 2, yPos + 6)
|
|
||||||
currentX += colWidths[0] + colWidths[1]
|
|
||||||
|
|
||||||
pdf.text((paymentData.summary?.total_orders ?? 0).toString(), currentX + colWidths[2] / 2, yPos + 6, {
|
|
||||||
align: 'center'
|
|
||||||
})
|
|
||||||
currentX += colWidths[2]
|
|
||||||
|
|
||||||
pdf.text(this.formatCurrency(paymentData.summary?.total_amount ?? 0), currentX + colWidths[3] - 2, yPos + 6, {
|
|
||||||
align: 'right'
|
|
||||||
})
|
|
||||||
|
|
||||||
currentX += colWidths[3]
|
|
||||||
pdf.text('100.0%', currentX + colWidths[4] / 2, yPos + 6, { align: 'center' })
|
|
||||||
|
|
||||||
return yPos + 20
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Format currency for display
|
|
||||||
*/
|
|
||||||
private static formatCurrency(amount: number): string {
|
|
||||||
return `Rp ${amount.toLocaleString('id-ID')}`
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate filename with timestamp
|
|
||||||
*/
|
|
||||||
private static generateFilename(prefix: string, extension: string): string {
|
|
||||||
const now = new Date()
|
|
||||||
const year = now.getFullYear()
|
|
||||||
const month = (now.getMonth() + 1).toString().padStart(2, '0')
|
|
||||||
const day = now.getDate().toString().padStart(2, '0')
|
|
||||||
const hour = now.getHours().toString().padStart(2, '0')
|
|
||||||
const minute = now.getMinutes().toString().padStart(2, '0')
|
|
||||||
|
|
||||||
return `${prefix}_${year}_${month}_${day}_${hour}${minute}.${extension}`
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Export Payment Method data with custom configuration
|
|
||||||
*/
|
|
||||||
static async exportCustomPaymentToPDF(
|
|
||||||
paymentData: PaymentReport,
|
|
||||||
options?: {
|
|
||||||
title?: string
|
|
||||||
includeSummary?: boolean
|
|
||||||
customFilename?: string
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const jsPDFModule = await import('jspdf')
|
|
||||||
const jsPDF = jsPDFModule.default
|
|
||||||
|
|
||||||
const pdf = new jsPDF('p', 'mm', 'a4')
|
|
||||||
|
|
||||||
let yPos = 20
|
|
||||||
const pageWidth = pdf.internal.pageSize.getWidth()
|
|
||||||
const pageHeight = pdf.internal.pageSize.getHeight()
|
|
||||||
const marginLeft = 20
|
|
||||||
const marginRight = 20
|
|
||||||
const marginBottom = 15
|
|
||||||
|
|
||||||
const checkPageBreak = (neededSpace: number) => {
|
|
||||||
if (yPos + neededSpace > pageHeight - marginBottom) {
|
|
||||||
pdf.addPage()
|
|
||||||
yPos = 20
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Custom title if provided
|
|
||||||
if (options?.title) {
|
|
||||||
pdf.setFontSize(20)
|
|
||||||
pdf.setFont('helvetica', 'bold')
|
|
||||||
pdf.setTextColor(0, 0, 0)
|
|
||||||
pdf.text(options.title, pageWidth / 2, yPos, { align: 'center' })
|
|
||||||
yPos += 15
|
|
||||||
} else {
|
|
||||||
yPos = this.addReportTitle(pdf, paymentData, yPos, pageWidth, marginLeft, marginRight)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Optional summary section
|
|
||||||
if (options?.includeSummary !== false) {
|
|
||||||
checkPageBreak(50)
|
|
||||||
yPos = this.addRingkasanSection(pdf, paymentData, yPos, pageWidth, marginLeft, marginRight, checkPageBreak)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Payment methods detail
|
|
||||||
checkPageBreak(80)
|
|
||||||
yPos = this.addPaymentMethodsSection(pdf, paymentData, yPos, pageWidth, marginLeft, marginRight, checkPageBreak)
|
|
||||||
|
|
||||||
const exportFilename = options?.customFilename || this.generateFilename('Custom_Payment_Report', 'pdf')
|
|
||||||
pdf.save(exportFilename)
|
|
||||||
|
|
||||||
return { success: true, filename: exportFilename }
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error exporting custom payment report to PDF:', error)
|
|
||||||
return { success: false, error: `PDF export failed: ${(error as Error).message}` }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,439 +0,0 @@
|
|||||||
// services/pdfExportProductService.ts
|
|
||||||
import { ProductSalesReport } from '@/types/services/analytic'
|
|
||||||
|
|
||||||
export class PDFExportSalesProductService {
|
|
||||||
/**
|
|
||||||
* Export Product Sales Report to PDF
|
|
||||||
*/
|
|
||||||
static async exportProductSalesToPDF(productData: ProductSalesReport, 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.addProductReportContent(pdf, productData)
|
|
||||||
|
|
||||||
// Generate filename
|
|
||||||
const exportFilename = filename || this.generateFilename('Laporan_Penjualan_Produk', 'pdf')
|
|
||||||
|
|
||||||
// Save PDF
|
|
||||||
pdf.save(exportFilename)
|
|
||||||
|
|
||||||
return { success: true, filename: exportFilename }
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error exporting product report to PDF:', error)
|
|
||||||
return { success: false, error: `PDF export failed: ${(error as Error).message}` }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add product report content to PDF
|
|
||||||
*/
|
|
||||||
private static async addProductReportContent(pdf: any, productData: ProductSalesReport) {
|
|
||||||
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, productData, yPos, pageWidth, marginLeft, marginRight)
|
|
||||||
|
|
||||||
// Section 1: Ringkasan
|
|
||||||
checkPageBreak(50)
|
|
||||||
yPos = this.addRingkasanSection(pdf, productData, yPos, pageWidth, marginLeft, marginRight, checkPageBreak)
|
|
||||||
|
|
||||||
// Section 2: Product Details
|
|
||||||
checkPageBreak(100)
|
|
||||||
yPos = this.addProductDetailsSection(pdf, productData, yPos, pageWidth, marginLeft, marginRight, checkPageBreak)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add report title
|
|
||||||
*/
|
|
||||||
private static addReportTitle(
|
|
||||||
pdf: any,
|
|
||||||
productData: ProductSalesReport,
|
|
||||||
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 Produk', pageWidth / 2, yPos, { align: 'center' })
|
|
||||||
yPos += 10
|
|
||||||
|
|
||||||
// Period
|
|
||||||
pdf.setFontSize(12)
|
|
||||||
pdf.setFont('helvetica', 'normal')
|
|
||||||
const periodText = `${productData.date_from.split('T')[0]} - ${productData.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,
|
|
||||||
productData: ProductSalesReport,
|
|
||||||
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 productSummary = {
|
|
||||||
totalQuantitySold: productData.data?.reduce((sum, item) => sum + (item?.quantity_sold || 0), 0) || 0,
|
|
||||||
totalRevenue: productData.data?.reduce((sum, item) => sum + (item?.revenue || 0), 0) || 0,
|
|
||||||
totalOrders: productData.data?.reduce((sum, item) => sum + (item?.order_count || 0), 0) || 0,
|
|
||||||
averageRevenue: 0,
|
|
||||||
totalProducts: productData.data?.length || 0
|
|
||||||
}
|
|
||||||
productSummary.averageRevenue =
|
|
||||||
productSummary.totalQuantitySold > 0 ? productSummary.totalRevenue / productSummary.totalQuantitySold : 0
|
|
||||||
|
|
||||||
const ringkasanItems = [
|
|
||||||
{ label: 'Total Produk', value: productSummary.totalProducts.toString(), bold: false },
|
|
||||||
{ label: 'Total Quantity Sold', value: productSummary.totalQuantitySold.toString(), bold: false },
|
|
||||||
{ label: 'Total Orders', value: productSummary.totalOrders.toString(), bold: false },
|
|
||||||
{ label: 'Total Revenue', value: this.formatCurrency(productSummary.totalRevenue), bold: true },
|
|
||||||
{ label: 'Average Revenue per Item', value: this.formatCurrency(productSummary.averageRevenue), 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 Product Details section - SAMA STYLE SEPERTI SALES REPORT
|
|
||||||
*/
|
|
||||||
private static addProductDetailsSection(
|
|
||||||
pdf: any,
|
|
||||||
productData: ProductSalesReport,
|
|
||||||
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 Produk', marginLeft, yPos)
|
|
||||||
yPos += 12
|
|
||||||
|
|
||||||
// Reset formatting
|
|
||||||
pdf.setTextColor(0, 0, 0)
|
|
||||||
|
|
||||||
// Table setup
|
|
||||||
const tableWidth = pageWidth - marginLeft - marginRight
|
|
||||||
const colWidths = [60, 20, 20, 30, 30] // Product, Qty, Order, Revenue, Average
|
|
||||||
let currentX = marginLeft
|
|
||||||
|
|
||||||
// Table header
|
|
||||||
pdf.setFillColor(240, 240, 240)
|
|
||||||
pdf.rect(marginLeft, yPos, tableWidth, 10, 'F')
|
|
||||||
|
|
||||||
pdf.setFont('helvetica', 'bold')
|
|
||||||
pdf.setFontSize(9)
|
|
||||||
|
|
||||||
const headers = ['Produk', 'Qty', 'Order', 'Pendapatan', 'Rata Rata']
|
|
||||||
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
|
|
||||||
|
|
||||||
// Group products by category
|
|
||||||
const groupedProducts =
|
|
||||||
productData.data?.reduce(
|
|
||||||
(acc, item) => {
|
|
||||||
const categoryName = item.category_name || 'Tidak Berkategori'
|
|
||||||
if (!acc[categoryName]) {
|
|
||||||
acc[categoryName] = []
|
|
||||||
}
|
|
||||||
acc[categoryName].push(item)
|
|
||||||
return acc
|
|
||||||
},
|
|
||||||
{} as Record<string, any[]>
|
|
||||||
) || {}
|
|
||||||
|
|
||||||
// Calculate product summary
|
|
||||||
const productSummary = {
|
|
||||||
totalQuantitySold: productData.data?.reduce((sum, item) => sum + (item?.quantity_sold || 0), 0) || 0,
|
|
||||||
totalRevenue: productData.data?.reduce((sum, item) => sum + (item?.revenue || 0), 0) || 0,
|
|
||||||
totalOrders: productData.data?.reduce((sum, item) => sum + (item?.order_count || 0), 0) || 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Table rows
|
|
||||||
pdf.setFont('helvetica', 'normal')
|
|
||||||
pdf.setFontSize(9)
|
|
||||||
|
|
||||||
// Render grouped products
|
|
||||||
Object.keys(groupedProducts)
|
|
||||||
.sort()
|
|
||||||
.forEach(categoryName => {
|
|
||||||
const categoryProducts = groupedProducts[categoryName]
|
|
||||||
|
|
||||||
// Check page break for category header
|
|
||||||
if (checkPageBreak(10)) yPos = 20
|
|
||||||
|
|
||||||
// Category header
|
|
||||||
pdf.setFillColor(248, 248, 248) // Soft background
|
|
||||||
pdf.rect(marginLeft, yPos, tableWidth, 10, 'F')
|
|
||||||
pdf.setFont('helvetica', 'bold')
|
|
||||||
pdf.setFontSize(9)
|
|
||||||
pdf.setTextColor(102, 45, 145)
|
|
||||||
pdf.text(categoryName.toUpperCase(), marginLeft + 2, yPos + 6)
|
|
||||||
pdf.setTextColor(0, 0, 0)
|
|
||||||
yPos += 10
|
|
||||||
|
|
||||||
// Category products
|
|
||||||
categoryProducts.forEach((item, index) => {
|
|
||||||
if (checkPageBreak(10)) yPos = 20
|
|
||||||
|
|
||||||
currentX = marginLeft
|
|
||||||
pdf.setFont('helvetica', 'normal')
|
|
||||||
pdf.setFontSize(9)
|
|
||||||
pdf.setTextColor(0, 0, 0)
|
|
||||||
|
|
||||||
// Product name (indented and truncated if needed)
|
|
||||||
const productName =
|
|
||||||
item.product_name.length > 45 ? item.product_name.substring(0, 42) + '...' : item.product_name
|
|
||||||
|
|
||||||
pdf.text(` ${productName}`, currentX + 2, yPos + 5) // Indented for products
|
|
||||||
currentX += colWidths[0]
|
|
||||||
|
|
||||||
// Quantity
|
|
||||||
pdf.text(item.quantity_sold.toString(), currentX + colWidths[1] / 2, yPos + 5, { align: 'center' })
|
|
||||||
currentX += colWidths[1]
|
|
||||||
|
|
||||||
// Order count
|
|
||||||
pdf.text((item.order_count || 0).toString(), currentX + colWidths[2] / 2, yPos + 5, { align: 'center' })
|
|
||||||
currentX += colWidths[2]
|
|
||||||
|
|
||||||
// Revenue
|
|
||||||
pdf.text(this.formatCurrency(item.revenue), currentX + colWidths[3] - 2, yPos + 5, { align: 'right' })
|
|
||||||
currentX += colWidths[3]
|
|
||||||
|
|
||||||
// Average price
|
|
||||||
pdf.text(this.formatCurrency(item.average_price), 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
|
|
||||||
})
|
|
||||||
|
|
||||||
// Category subtotal
|
|
||||||
if (checkPageBreak(10)) yPos = 20
|
|
||||||
|
|
||||||
const categoryTotalQty = categoryProducts.reduce((sum, item) => sum + (item.quantity_sold || 0), 0)
|
|
||||||
const categoryTotalOrders = categoryProducts.reduce((sum, item) => sum + (item.order_count || 0), 0)
|
|
||||||
const categoryTotalRevenue = categoryProducts.reduce((sum, item) => sum + (item.revenue || 0), 0)
|
|
||||||
|
|
||||||
pdf.setFillColor(240, 240, 240) // Sama dengan table header
|
|
||||||
pdf.rect(marginLeft, yPos, tableWidth, 10, 'F')
|
|
||||||
pdf.setFont('helvetica', 'bold')
|
|
||||||
pdf.setFontSize(9)
|
|
||||||
|
|
||||||
currentX = marginLeft
|
|
||||||
pdf.text(`Subtotal ${categoryName}`, currentX + 2, yPos + 6)
|
|
||||||
currentX += colWidths[0]
|
|
||||||
|
|
||||||
pdf.text(categoryTotalQty.toString(), currentX + colWidths[1] / 2, yPos + 6, { align: 'center' })
|
|
||||||
currentX += colWidths[1]
|
|
||||||
|
|
||||||
pdf.text(categoryTotalOrders.toString(), currentX + colWidths[2] / 2, yPos + 6, { align: 'center' })
|
|
||||||
currentX += colWidths[2]
|
|
||||||
|
|
||||||
pdf.text(this.formatCurrency(categoryTotalRevenue), currentX + colWidths[3] - 2, yPos + 6, { align: 'right' })
|
|
||||||
|
|
||||||
yPos += 10
|
|
||||||
})
|
|
||||||
|
|
||||||
// Grand total
|
|
||||||
if (checkPageBreak(10)) yPos = 20
|
|
||||||
|
|
||||||
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 KESELURUHAN', currentX + 2, yPos + 6)
|
|
||||||
currentX += colWidths[0]
|
|
||||||
|
|
||||||
pdf.text(productSummary.totalQuantitySold.toString(), currentX + colWidths[1] / 2, yPos + 6, { align: 'center' })
|
|
||||||
currentX += colWidths[1]
|
|
||||||
|
|
||||||
pdf.text(productSummary.totalOrders.toString(), currentX + colWidths[2] / 2, yPos + 6, { align: 'center' })
|
|
||||||
currentX += colWidths[2]
|
|
||||||
|
|
||||||
pdf.text(this.formatCurrency(productSummary.totalRevenue), currentX + colWidths[3] - 2, yPos + 6, {
|
|
||||||
align: 'right'
|
|
||||||
})
|
|
||||||
|
|
||||||
return yPos + 25
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 Product Sales data with custom configuration
|
|
||||||
*/
|
|
||||||
static async exportCustomProductToPDF(
|
|
||||||
productData: ProductSalesReport,
|
|
||||||
options?: {
|
|
||||||
title?: string
|
|
||||||
includeSummary?: boolean
|
|
||||||
customFilename?: string
|
|
||||||
groupByCategory?: 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, productData, yPos, pageWidth, marginLeft, marginRight)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Optional summary section
|
|
||||||
if (options?.includeSummary !== false) {
|
|
||||||
checkPageBreak(50)
|
|
||||||
yPos = this.addRingkasanSection(pdf, productData, yPos, pageWidth, marginLeft, marginRight, checkPageBreak)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Product details
|
|
||||||
checkPageBreak(100)
|
|
||||||
yPos = this.addProductDetailsSection(pdf, productData, yPos, pageWidth, marginLeft, marginRight, checkPageBreak)
|
|
||||||
|
|
||||||
const exportFilename = options?.customFilename || this.generateFilename('Custom_Product_Sales', 'pdf')
|
|
||||||
pdf.save(exportFilename)
|
|
||||||
|
|
||||||
return { success: true, filename: exportFilename }
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error exporting custom product report to PDF:', error)
|
|
||||||
return { success: false, error: `PDF export failed: ${(error as Error).message}` }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -3,16 +3,14 @@
|
|||||||
import DateRangePicker from '@/components/RangeDatePicker'
|
import DateRangePicker from '@/components/RangeDatePicker'
|
||||||
import { ReportItemHeader, ReportItemSubheader } from '@/components/report/ReportItem'
|
import { ReportItemHeader, ReportItemSubheader } from '@/components/report/ReportItem'
|
||||||
import { ExcelExportPaymentService } from '@/services/export/excel/ExcelExportPaymentService'
|
import { ExcelExportPaymentService } from '@/services/export/excel/ExcelExportPaymentService'
|
||||||
import { PDFExportPaymentService } from '@/services/export/pdf/PDFExportPaymentService'
|
|
||||||
import { usePaymentAnalytics } from '@/services/queries/analytics'
|
import { usePaymentAnalytics } from '@/services/queries/analytics'
|
||||||
import { formatCurrency, formatDateDDMMYYYY } from '@/utils/transform'
|
import { formatCurrency, formatDateDDMMYYYY } from '@/utils/transform'
|
||||||
import { Button, Card, CardContent, Menu, MenuItem } from '@mui/material'
|
import { Button, Card, CardContent } from '@mui/material'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
|
||||||
const ReportPaymentMethodContent = () => {
|
const ReportPaymentMethodContent = () => {
|
||||||
const [startDate, setStartDate] = useState<Date | null>(new Date())
|
const [startDate, setStartDate] = useState<Date | null>(new Date())
|
||||||
const [endDate, setEndDate] = useState<Date | null>(new Date())
|
const [endDate, setEndDate] = useState<Date | null>(new Date())
|
||||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)
|
|
||||||
|
|
||||||
const { data: paymentAnalytics } = usePaymentAnalytics({
|
const { data: paymentAnalytics } = usePaymentAnalytics({
|
||||||
date_from: formatDateDDMMYYYY(startDate!),
|
date_from: formatDateDDMMYYYY(startDate!),
|
||||||
@ -40,38 +38,6 @@ const ReportPaymentMethodContent = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleExportPDF = async () => {
|
|
||||||
if (!paymentAnalytics) {
|
|
||||||
console.warn('No data available for export')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await PDFExportPaymentService.exportPaymentMethodToPDF(
|
|
||||||
paymentAnalytics,
|
|
||||||
`Laporan_Metode_Pembayaran_${formatDateDDMMYYYY(startDate!)}_${formatDateDDMMYYYY(endDate!)}.pdf`
|
|
||||||
)
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
console.log(`PDF exported successfully: ${result.filename}`)
|
|
||||||
// Optional: Show success message to user
|
|
||||||
} else {
|
|
||||||
console.error('PDF export failed:', result.error)
|
|
||||||
// Optional: Show error message to user
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('PDF export error:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleExportClick = (event: React.MouseEvent<HTMLElement>) => {
|
|
||||||
setAnchorEl(event.currentTarget)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleExportClose = () => {
|
|
||||||
setAnchorEl(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<div className='p-6 border-be'>
|
<div className='p-6 border-be'>
|
||||||
@ -80,30 +46,11 @@ const ReportPaymentMethodContent = () => {
|
|||||||
color='secondary'
|
color='secondary'
|
||||||
variant='tonal'
|
variant='tonal'
|
||||||
startIcon={<i className='tabler-upload' />}
|
startIcon={<i className='tabler-upload' />}
|
||||||
endIcon={<i className='tabler-chevron-down' />}
|
|
||||||
className='max-sm:is-full'
|
className='max-sm:is-full'
|
||||||
onClick={handleExportClick}
|
onClick={handleExportExcel}
|
||||||
>
|
>
|
||||||
Ekspor
|
Ekspor
|
||||||
</Button>
|
</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
|
<DateRangePicker
|
||||||
startDate={startDate}
|
startDate={startDate}
|
||||||
endDate={endDate}
|
endDate={endDate}
|
||||||
|
|||||||
@ -2,17 +2,14 @@
|
|||||||
|
|
||||||
import DateRangePicker from '@/components/RangeDatePicker'
|
import DateRangePicker from '@/components/RangeDatePicker'
|
||||||
import { ReportItemHeader, ReportItemSubheader } from '@/components/report/ReportItem'
|
import { ReportItemHeader, ReportItemSubheader } from '@/components/report/ReportItem'
|
||||||
import { ExcelExportSalesProductService } from '@/services/export/excel/ExcelExportSalesProductService'
|
|
||||||
import { PDFExportSalesProductService } from '@/services/export/pdf/PdfExportSalesProductSevice'
|
|
||||||
import { useProductSalesAnalytics } from '@/services/queries/analytics'
|
import { useProductSalesAnalytics } from '@/services/queries/analytics'
|
||||||
import { formatCurrency, formatDateDDMMYYYY } from '@/utils/transform'
|
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'
|
import { useState } from 'react'
|
||||||
|
|
||||||
const ReportSalesPerProductContent = () => {
|
const ReportSalesPerProductContent = () => {
|
||||||
const [startDate, setStartDate] = useState<Date | null>(new Date())
|
const [startDate, setStartDate] = useState<Date | null>(new Date())
|
||||||
const [endDate, setEndDate] = useState<Date | null>(new Date())
|
const [endDate, setEndDate] = useState<Date | null>(new Date())
|
||||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)
|
|
||||||
|
|
||||||
const { data: products } = useProductSalesAnalytics({
|
const { data: products } = useProductSalesAnalytics({
|
||||||
date_from: formatDateDDMMYYYY(startDate!),
|
date_from: formatDateDDMMYYYY(startDate!),
|
||||||
@ -25,58 +22,6 @@ const ReportSalesPerProductContent = () => {
|
|||||||
totalOrders: products?.data?.reduce((sum, item) => sum + (item?.order_count || 0), 0) || 0
|
totalOrders: products?.data?.reduce((sum, item) => sum + (item?.order_count || 0), 0) || 0
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleExportClick = (event: React.MouseEvent<HTMLElement>) => {
|
|
||||||
setAnchorEl(event.currentTarget)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleExportClose = () => {
|
|
||||||
setAnchorEl(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleExportExcel = async () => {
|
|
||||||
if (!products) {
|
|
||||||
console.warn('No data available for export')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await ExcelExportSalesProductService.exportProductSalesToExcel(
|
|
||||||
products,
|
|
||||||
`Penjualan_Produk_${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 (!products) {
|
|
||||||
console.warn('No data available for export')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await PDFExportSalesProductService.exportProductSalesToPDF(
|
|
||||||
products,
|
|
||||||
`Laporan_Penjualan_Produk_${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 (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<div className='p-6 border-be'>
|
<div className='p-6 border-be'>
|
||||||
@ -85,30 +30,11 @@ const ReportSalesPerProductContent = () => {
|
|||||||
color='secondary'
|
color='secondary'
|
||||||
variant='tonal'
|
variant='tonal'
|
||||||
startIcon={<i className='tabler-upload' />}
|
startIcon={<i className='tabler-upload' />}
|
||||||
endIcon={<i className='tabler-chevron-down' />}
|
|
||||||
className='max-sm:is-full'
|
className='max-sm:is-full'
|
||||||
onClick={handleExportClick}
|
// onClick={handleExportPDF}
|
||||||
>
|
>
|
||||||
Ekspor
|
Ekspor
|
||||||
</Button>
|
</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
|
<DateRangePicker
|
||||||
startDate={startDate}
|
startDate={startDate}
|
||||||
endDate={endDate}
|
endDate={endDate}
|
||||||
|
|||||||
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import DateRangePicker from '@/components/RangeDatePicker'
|
import DateRangePicker from '@/components/RangeDatePicker'
|
||||||
import { ReportItem, ReportItemFooter, ReportItemHeader, ReportItemSubheader } from '@/components/report/ReportItem'
|
import { ReportItem, ReportItemFooter, ReportItemHeader, ReportItemSubheader } from '@/components/report/ReportItem'
|
||||||
import { ExcelExportSalesService } from '@/services/export/excel/ExcelExportSalesService'
|
|
||||||
import { PDFExportSalesService } from '@/services/export/pdf/PDFExportSalesService'
|
import { PDFExportSalesService } from '@/services/export/pdf/PDFExportSalesService'
|
||||||
import {
|
import {
|
||||||
useCategoryAnalytics,
|
useCategoryAnalytics,
|
||||||
@ -11,13 +10,12 @@ import {
|
|||||||
useProfitLossAnalytics
|
useProfitLossAnalytics
|
||||||
} from '@/services/queries/analytics'
|
} from '@/services/queries/analytics'
|
||||||
import { formatCurrency, formatDateDDMMYYYY } from '@/utils/transform'
|
import { formatCurrency, formatDateDDMMYYYY } from '@/utils/transform'
|
||||||
import { Button, Card, CardContent, Menu, MenuItem, Paper } from '@mui/material'
|
import { Button, Card, CardContent, Paper } from '@mui/material'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
|
||||||
const ReportSalesContent = () => {
|
const ReportSalesContent = () => {
|
||||||
const [startDate, setStartDate] = useState<Date | null>(new Date())
|
const [startDate, setStartDate] = useState<Date | null>(new Date())
|
||||||
const [endDate, setEndDate] = useState<Date | null>(new Date())
|
const [endDate, setEndDate] = useState<Date | null>(new Date())
|
||||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)
|
|
||||||
|
|
||||||
const { data: profitLoss } = useProfitLossAnalytics({
|
const { data: profitLoss } = useProfitLossAnalytics({
|
||||||
date_from: formatDateDDMMYYYY(startDate!),
|
date_from: formatDateDDMMYYYY(startDate!),
|
||||||
@ -76,38 +74,6 @@ const ReportSalesContent = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleExportExcel = async () => {
|
|
||||||
try {
|
|
||||||
const salesData = {
|
|
||||||
profitLoss: profitLoss!,
|
|
||||||
paymentAnalytics: paymentAnalytics!,
|
|
||||||
categoryAnalytics: category!,
|
|
||||||
productAnalytics: products!
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await ExcelExportSalesService.exportSalesReportToExcel(salesData)
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
console.log('Excel export successful:', result.filename)
|
|
||||||
// Optional: Show success notification
|
|
||||||
} else {
|
|
||||||
console.error('Excel export failed:', result.error)
|
|
||||||
alert('Export Excel gagal. Silakan coba lagi.')
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Excel export error:', error)
|
|
||||||
alert('Terjadi kesalahan saat export Excel.')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleExportClick = (event: React.MouseEvent<HTMLElement>) => {
|
|
||||||
setAnchorEl(event.currentTarget)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleExportClose = () => {
|
|
||||||
setAnchorEl(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<div className='p-6 border-be'>
|
<div className='p-6 border-be'>
|
||||||
@ -116,30 +82,11 @@ const ReportSalesContent = () => {
|
|||||||
color='secondary'
|
color='secondary'
|
||||||
variant='tonal'
|
variant='tonal'
|
||||||
startIcon={<i className='tabler-upload' />}
|
startIcon={<i className='tabler-upload' />}
|
||||||
endIcon={<i className='tabler-chevron-down' />}
|
|
||||||
className='max-sm:is-full'
|
className='max-sm:is-full'
|
||||||
onClick={handleExportClick}
|
onClick={handleExportPDF}
|
||||||
>
|
>
|
||||||
Ekspor
|
Ekspor
|
||||||
</Button>
|
</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
|
<DateRangePicker
|
||||||
startDate={startDate}
|
startDate={startDate}
|
||||||
endDate={endDate}
|
endDate={endDate}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user