From 82e79463b51526d2c148eb73006f368045e96143 Mon Sep 17 00:00:00 2001 From: efrilm Date: Fri, 26 Sep 2025 13:47:04 +0700 Subject: [PATCH] Report Sales Order --- .../excel/ExcelExportSalesOrderService.ts | 335 +++++++++++ .../export/pdf/PDFExportSalesOrderService.ts | 529 ++++++++++++++++++ .../sales-order/ReportSalesOrderContent.tsx | 78 ++- 3 files changed, 940 insertions(+), 2 deletions(-) create mode 100644 src/services/export/excel/ExcelExportSalesOrderService.ts create mode 100644 src/services/export/pdf/PDFExportSalesOrderService.ts diff --git a/src/services/export/excel/ExcelExportSalesOrderService.ts b/src/services/export/excel/ExcelExportSalesOrderService.ts new file mode 100644 index 0000000..e8a6f24 --- /dev/null +++ b/src/services/export/excel/ExcelExportSalesOrderService.ts @@ -0,0 +1,335 @@ +// services/excelExportSalesOrderService.ts +import type { SalesReport } from '@/types/services/analytic' + +export class ExcelExportSalesOrderService { + /** + * Export Sales Order Report to Excel + */ + static async exportSalesOrderToExcel(salesData: SalesReport, filename?: string) { + try { + // Dynamic import untuk xlsx library + const XLSX = await import('xlsx') + + // Prepare data untuk Excel + const worksheetData: any[][] = [] + + // Header dengan report info (baris 1-2) + worksheetData.push(['LAPORAN PESANAN PENJUALAN']) // Row 0 - Main title + worksheetData.push([`Periode: ${salesData.date_from.split('T')[0]} - ${salesData.date_to.split('T')[0]}`]) // Row 1 - Period + worksheetData.push([]) // Empty row + + // Add Summary Section + worksheetData.push(['RINGKASAN PERIODE']) // Section header + worksheetData.push([]) // Empty row + + const summaryData = [ + ['Total Sales:', `Rp ${salesData.summary.total_sales.toLocaleString('id-ID')}`], + ['Total Orders:', salesData.summary.total_orders.toString()], + ['Total Items:', salesData.summary.total_items.toString()], + ['Average Order Value:', `Rp ${salesData.summary.average_order_value.toLocaleString('id-ID')}`], + ['Total Tax:', `Rp ${salesData.summary.total_tax.toLocaleString('id-ID')}`], + ['Total Discount:', `Rp ${salesData.summary.total_discount.toLocaleString('id-ID')}`], + ['Net Sales:', `Rp ${salesData.summary.net_sales.toLocaleString('id-ID')}`] + ] + + summaryData.forEach(row => { + worksheetData.push([row[0], row[1]]) // Only 2 columns needed + }) + + worksheetData.push([]) // Empty row + worksheetData.push([]) // Empty row + + // Daily Sales Section Header + worksheetData.push(['RINCIAN HARIAN']) // Section header + worksheetData.push([]) // Empty row + + // Header row untuk tabel daily sales data + const headerRow = ['No', 'Tanggal', 'Penjualan', 'Pesanan', 'Qty', 'Pajak', 'Diskon', 'Pendapatan'] + worksheetData.push(headerRow) + + // Add daily sales data rows + salesData.data?.forEach((dailySales, index) => { + const rowData = [ + index + 1, // No + this.formatDate(dailySales.date), + dailySales.sales, // Store as number for Excel formatting + dailySales.orders, + dailySales.items, + dailySales.tax, // Store as number for Excel formatting + dailySales.discount, // Store as number for Excel formatting + dailySales.net_sales // Store as number for Excel formatting + ] + worksheetData.push(rowData) + }) + + // Add total row + const totalRow = [ + 'TOTAL', + '', + salesData.summary.total_sales, + salesData.summary.total_orders, + salesData.summary.total_items, + salesData.summary.total_tax, + salesData.summary.total_discount, + salesData.summary.net_sales + ] + worksheetData.push(totalRow) + + // Create workbook dan worksheet + const workbook = XLSX.utils.book_new() + const worksheet = XLSX.utils.aoa_to_sheet(worksheetData) + + // Apply basic formatting + this.applyBasicFormatting(worksheet, worksheetData.length, XLSX) + + // Add worksheet ke workbook + XLSX.utils.book_append_sheet(workbook, worksheet, 'Pesanan Penjualan') + + // Generate filename + const exportFilename = filename || this.generateFilename('Pesanan_Penjualan') + + // Download file + XLSX.writeFile(workbook, exportFilename) + + return { success: true, filename: exportFilename } + } catch (error) { + console.error('Error exporting to Excel:', error) + return { success: false, error: 'Failed to export Excel file' } + } + } + + /** + * Apply basic formatting (SheetJS compatible) + */ + private static applyBasicFormatting(worksheet: any, totalRows: number, XLSX: any) { + // Set column widths + const colWidths = [ + { wch: 8 }, // No + { wch: 15 }, // Tanggal + { wch: 18 }, // Penjualan + { wch: 12 }, // Pesanan + { wch: 10 }, // Qty + { wch: 15 }, // Pajak + { wch: 15 }, // Diskon + { wch: 18 } // Pendapatan + ] + worksheet['!cols'] = colWidths + + // Set row heights for better spacing + worksheet['!rows'] = [ + { hpt: 30 }, // Title row + { hpt: 25 }, // Period row + { hpt: 15 }, // Empty row + { hpt: 25 }, // Summary header + { hpt: 15 } // Empty row + ] + + // Merge cells untuk headers + const merges = [ + { s: { r: 0, c: 0 }, e: { r: 0, c: 7 } }, // Title (span across all columns) + { s: { r: 1, c: 0 }, e: { r: 1, c: 7 } }, // Period (span across all columns) + { s: { r: 3, c: 0 }, e: { r: 3, c: 7 } } // Summary header (span across all columns) + ] + + // Find and add merge for daily sales header + for (let i = 0; i < totalRows; i++) { + const cell = worksheet[XLSX.utils.encode_cell({ r: i, c: 0 })] + if (cell && cell.v === 'RINCIAN HARIAN') { + merges.push({ s: { r: i, c: 0 }, e: { r: i, c: 7 } }) // Span across all columns + break + } + } + + worksheet['!merges'] = merges + + // Apply number formatting untuk currency cells + this.applyNumberFormatting(worksheet, totalRows, XLSX) + } + + /** + * Apply number formatting for currency and styling + */ + private static applyNumberFormatting(worksheet: any, totalRows: number, XLSX: any) { + // Find table data start (after header row) + let dataStartRow = -1 + for (let i = 0; i < totalRows; i++) { + const cell = worksheet[XLSX.utils.encode_cell({ r: i, c: 0 })] + if (cell && cell.v === 'No') { + dataStartRow = i + 1 + break + } + } + + if (dataStartRow === -1) return + + // Count actual data rows (excluding total row) + const dataRowsCount = totalRows - dataStartRow - 1 // -1 for total row + + // Apply currency formatting to currency columns (columns 2, 5, 6, 7 - Penjualan, Pajak, Diskon, Pendapatan) + const currencyColumns = [2, 5, 6, 7] + for (let row = dataStartRow; row <= dataStartRow + dataRowsCount; row++) { + // Include total row + currencyColumns.forEach(col => { + const cellAddress = XLSX.utils.encode_cell({ r: row, c: col }) + const cell = worksheet[cellAddress] + + if (cell && typeof cell.v === 'number') { + // Apply Indonesian currency format + cell.z = '#,##0' + cell.t = 'n' + } + }) + } + + // Apply styling to header row + const headerRowIndex = dataStartRow - 1 + for (let col = 0; col < 8; col++) { + const cellAddress = XLSX.utils.encode_cell({ r: headerRowIndex, c: col }) + const cell = worksheet[cellAddress] + + if (cell) { + // Apply bold formatting (basic approach for SheetJS) + cell.s = { + font: { bold: true }, + fill: { fgColor: { rgb: 'F3F4F6' } }, // Light gray background + border: { + bottom: { style: 'medium', color: { rgb: '000000' } } + } + } + } + } + + // Apply styling to total row + const totalRowIndex = dataStartRow + dataRowsCount + for (let col = 0; col < 8; col++) { + const cellAddress = XLSX.utils.encode_cell({ r: totalRowIndex, c: col }) + const cell = worksheet[cellAddress] + + if (cell) { + // Apply bold formatting for total row + cell.s = { + font: { bold: true }, + border: { + top: { style: 'medium', color: { rgb: '000000' } } + } + } + } + } + } + + /** + * Format date for display + */ + private static formatDate(dateString: string): string { + const date = new Date(dateString) + return date.toLocaleDateString('id-ID', { + day: '2-digit', + month: '2-digit', + year: 'numeric' + }) + } + + /** + * Generate filename with timestamp + */ + private static generateFilename(prefix: string): string { + const now = new Date() + const year = now.getFullYear() + const month = (now.getMonth() + 1).toString().padStart(2, '0') + const day = now.getDate().toString().padStart(2, '0') + const hour = now.getHours().toString().padStart(2, '0') + const minute = now.getMinutes().toString().padStart(2, '0') + + return `${prefix}_${year}_${month}_${day}_${hour}${minute}.xlsx` + } + + /** + * Export Sales Order data with custom configuration + */ + static async exportCustomSalesOrderData( + salesData: SalesReport, + options?: { + includeSummary?: boolean + includeItemsColumn?: boolean + customFilename?: string + sheetName?: string + } + ) { + try { + const XLSX = await import('xlsx') + const worksheetData: any[][] = [] + + // Always include title and period + worksheetData.push(['LAPORAN PESANAN PENJUALAN']) + worksheetData.push([`Periode: ${salesData.date_from.split('T')[0]} - ${salesData.date_to.split('T')[0]}`]) + worksheetData.push([]) + + // Optional summary + if (options?.includeSummary !== false) { + worksheetData.push(['RINGKASAN PERIODE']) + worksheetData.push([]) + const summaryData = [ + ['Total Sales:', `Rp ${salesData.summary.total_sales.toLocaleString('id-ID')}`], + ['Total Orders:', salesData.summary.total_orders.toString()], + ['Total Items:', salesData.summary.total_items.toString()], + ['Net Sales:', `Rp ${salesData.summary.net_sales.toLocaleString('id-ID')}`] + ] + summaryData.forEach(row => worksheetData.push([row[0], row[1]])) + worksheetData.push([]) + worksheetData.push([]) + } + + worksheetData.push(['RINCIAN HARIAN']) + worksheetData.push([]) + + // Header row based on options + const headerRow = + options?.includeItemsColumn !== false + ? ['No', 'Tanggal', 'Penjualan', 'Pesanan', 'Qty', 'Pajak', 'Diskon', 'Pendapatan'] + : ['No', 'Tanggal', 'Penjualan', 'Pesanan', 'Pajak', 'Diskon', 'Pendapatan'] + worksheetData.push(headerRow) + + // Add daily data based on options + salesData.data?.forEach((dailySales, index) => { + const rowData = + options?.includeItemsColumn !== false + ? [ + index + 1, + this.formatDate(dailySales.date), + dailySales.sales, + dailySales.orders, + dailySales.items, + dailySales.tax, + dailySales.discount, + dailySales.net_sales + ] + : [ + index + 1, + this.formatDate(dailySales.date), + dailySales.sales, + dailySales.orders, + dailySales.tax, + dailySales.discount, + dailySales.net_sales + ] + worksheetData.push(rowData) + }) + + const workbook = XLSX.utils.book_new() + const worksheet = XLSX.utils.aoa_to_sheet(worksheetData) + + this.applyBasicFormatting(worksheet, worksheetData.length, XLSX) + + const sheetName = options?.sheetName || 'Pesanan Penjualan' + XLSX.utils.book_append_sheet(workbook, worksheet, sheetName) + + const exportFilename = options?.customFilename || this.generateFilename('Custom_Sales_Order') + XLSX.writeFile(workbook, exportFilename) + + return { success: true, filename: exportFilename } + } catch (error) { + console.error('Error exporting custom sales order data to Excel:', error) + return { success: false, error: 'Failed to export Excel file' } + } + } +} diff --git a/src/services/export/pdf/PDFExportSalesOrderService.ts b/src/services/export/pdf/PDFExportSalesOrderService.ts new file mode 100644 index 0000000..b1c1cbe --- /dev/null +++ b/src/services/export/pdf/PDFExportSalesOrderService.ts @@ -0,0 +1,529 @@ +// services/pdfExportSalesOrderService.ts +import { SalesReport } from '@/types/services/analytic' + +export class PDFExportSalesOrderService { + /** + * Export Sales Order Report to PDF + */ + static async exportSalesOrderToPDF(salesData: SalesReport, filename?: string) { + try { + // Dynamic import untuk jsPDF + const jsPDFModule = await import('jspdf') + const jsPDF = jsPDFModule.default + + // Create new PDF document - PORTRAIT A4 + const pdf = new jsPDF('p', 'mm', 'a4') + + // Add content + await this.addSalesOrderReportContent(pdf, salesData) + + // Generate filename + const exportFilename = filename || this.generateFilename('Laporan_Pesanan_Penjualan', 'pdf') + + // Save PDF + pdf.save(exportFilename) + + return { success: true, filename: exportFilename } + } catch (error) { + console.error('Error exporting sales order report to PDF:', error) + return { success: false, error: `PDF export failed: ${(error as Error).message}` } + } + } + + /** + * Add sales order report content to PDF + */ + private static async addSalesOrderReportContent(pdf: any, salesData: SalesReport) { + let yPos = 20 + const pageWidth = pdf.internal.pageSize.getWidth() + const pageHeight = pdf.internal.pageSize.getHeight() + const marginLeft = 20 + const marginRight = 20 + const marginBottom = 15 + + // Helper function to check page break + const checkPageBreak = (neededSpace: number) => { + if (yPos + neededSpace > pageHeight - marginBottom) { + pdf.addPage() + yPos = 20 + return true + } + return false + } + + // Title + yPos = this.addReportTitle(pdf, salesData, yPos, pageWidth, marginLeft, marginRight) + + // Section 1: Ringkasan + checkPageBreak(60) + yPos = this.addRingkasanSection(pdf, salesData, yPos, pageWidth, marginLeft, marginRight, checkPageBreak) + + // Section 2: Daily Sales Details + checkPageBreak(100) + yPos = this.addDailySalesSection(pdf, salesData, yPos, pageWidth, marginLeft, marginRight, checkPageBreak) + } + + /** + * Add report title + */ + private static addReportTitle( + pdf: any, + salesData: SalesReport, + startY: number, + pageWidth: number, + marginLeft: number, + marginRight: number + ): number { + let yPos = startY + + // Title + pdf.setFontSize(20) + pdf.setFont('helvetica', 'bold') + pdf.setTextColor(0, 0, 0) + pdf.text('Laporan Pesanan Penjualan', pageWidth / 2, yPos, { align: 'center' }) + yPos += 10 + + // Period + pdf.setFontSize(12) + pdf.setFont('helvetica', 'normal') + const periodText = `${salesData.date_from.split('T')[0]} - ${salesData.date_to.split('T')[0]}` + pdf.text(periodText, pageWidth / 2, yPos, { align: 'center' }) + yPos += 10 + + // Purple line separator + pdf.setDrawColor(102, 45, 145) + pdf.setLineWidth(2) + pdf.line(marginLeft, yPos, pageWidth - marginRight, yPos) + yPos += 15 + + return yPos + } + + /** + * Add Ringkasan section + */ + private static addRingkasanSection( + pdf: any, + salesData: SalesReport, + startY: number, + pageWidth: number, + marginLeft: number, + marginRight: number, + checkPageBreak: (space: number) => boolean + ): number { + let yPos = startY + + // Section title + pdf.setFontSize(14) + pdf.setFont('helvetica', 'bold') + pdf.setTextColor(102, 45, 145) + pdf.text('Ringkasan', marginLeft, yPos) + yPos += 12 + + // Reset text color + pdf.setTextColor(0, 0, 0) + pdf.setFontSize(11) + + const ringkasanItems = [ + { label: 'Total Sales', value: this.formatCurrency(salesData.summary.total_sales), bold: false }, + { label: 'Total Orders', value: salesData.summary.total_orders.toString(), bold: false }, + { label: 'Total Items', value: salesData.summary.total_items.toString(), bold: false }, + { label: 'Average Order Value', value: this.formatCurrency(salesData.summary.average_order_value), bold: false }, + { label: 'Total Tax', value: this.formatCurrency(salesData.summary.total_tax), bold: false }, + { label: 'Total Discount', value: this.formatCurrency(salesData.summary.total_discount), bold: false }, + { label: 'Net Sales', value: this.formatCurrency(salesData.summary.net_sales), bold: true } + ] + + ringkasanItems.forEach((item, index) => { + if (checkPageBreak(15)) yPos = 20 + + // Set font weight + pdf.setFont('helvetica', item.bold ? 'bold' : 'normal') + + pdf.text(item.label, marginLeft, yPos) + pdf.text(item.value, pageWidth - marginRight, yPos, { align: 'right' }) + + // Light separator line (except for bold row) + if (!item.bold) { + pdf.setDrawColor(230, 230, 230) + pdf.setLineWidth(0.5) + pdf.line(marginLeft, yPos + 2, pageWidth - marginRight, yPos + 2) + } + + yPos += 8 + }) + + return yPos + 20 + } + + /** + * Add Daily Sales section + */ + private static addDailySalesSection( + pdf: any, + salesData: SalesReport, + startY: number, + pageWidth: number, + marginLeft: number, + marginRight: number, + checkPageBreak: (space: number) => boolean + ): number { + let yPos = startY + + // Section title + pdf.setFontSize(14) + pdf.setFont('helvetica', 'bold') + pdf.setTextColor(102, 45, 145) + pdf.text('Rincian Harian', marginLeft, yPos) + yPos += 12 + + // Reset formatting + pdf.setTextColor(0, 0, 0) + + // Table setup - adjust for 7 columns + const tableWidth = pageWidth - marginLeft - marginRight + const colWidths = [20, 20, 25, 15, 20, 20, 25] // Date, Sales, Orders, Items, Tax, Discount, Net Sales + let currentX = marginLeft + + // Table header + pdf.setFillColor(240, 240, 240) + pdf.rect(marginLeft, yPos, tableWidth, 10, 'F') + + pdf.setFont('helvetica', 'bold') + pdf.setFontSize(8) // Smaller font for more columns + + const headers = ['Tanggal', 'Penjualan', 'Pesanan', 'Qty', 'Pajak', 'Diskon', 'Pendapatan'] + currentX = marginLeft + + headers.forEach((header, index) => { + if (index === 0) { + pdf.text(header, currentX + 2, yPos + 6) + } else { + pdf.text(header, currentX + colWidths[index] / 2, yPos + 6, { align: 'center' }) + } + currentX += colWidths[index] + }) + + yPos += 12 + + // Table rows + pdf.setFont('helvetica', 'normal') + pdf.setFontSize(8) + + salesData.data?.forEach((dailySales, index) => { + if (checkPageBreak(10)) yPos = 20 + + currentX = marginLeft + pdf.setFont('helvetica', 'normal') + pdf.setFontSize(8) + pdf.setTextColor(0, 0, 0) + + // Date + pdf.text(this.formatDate(dailySales.date), currentX + 2, yPos + 5) + currentX += colWidths[0] + + // Sales + pdf.text(this.formatCurrencyShort(dailySales.sales), currentX + colWidths[1] - 2, yPos + 5, { align: 'right' }) + currentX += colWidths[1] + + // Orders + pdf.text(dailySales.orders.toString(), currentX + colWidths[2] / 2, yPos + 5, { align: 'center' }) + currentX += colWidths[2] + + // Items + pdf.text(dailySales.items.toString(), currentX + colWidths[3] / 2, yPos + 5, { align: 'center' }) + currentX += colWidths[3] + + // Tax + pdf.text(this.formatCurrencyShort(dailySales.tax), currentX + colWidths[4] - 2, yPos + 5, { align: 'right' }) + currentX += colWidths[4] + + // Discount + pdf.text(this.formatCurrencyShort(dailySales.discount), currentX + colWidths[5] - 2, yPos + 5, { align: 'right' }) + currentX += colWidths[5] + + // Net Sales + pdf.text(this.formatCurrencyShort(dailySales.net_sales), currentX + colWidths[6] - 2, yPos + 5, { + align: 'right' + }) + + // Draw bottom border line + pdf.setDrawColor(230, 230, 230) + pdf.setLineWidth(0.3) + pdf.line(marginLeft, yPos + 8, marginLeft + tableWidth, yPos + 8) + + yPos += 10 + }) + + // Table footer (Total) + pdf.setFillColor(245, 245, 245) // Lighter gray + pdf.rect(marginLeft, yPos, tableWidth, 10, 'F') + + pdf.setFont('helvetica', 'bold') + pdf.setFontSize(8) + + currentX = marginLeft + pdf.text('TOTAL', currentX + 2, yPos + 6) + currentX += colWidths[0] + + pdf.text(this.formatCurrencyShort(salesData.summary.total_sales), currentX + colWidths[1] - 2, yPos + 6, { + align: 'right' + }) + currentX += colWidths[1] + + pdf.text(salesData.summary.total_orders.toString(), currentX + colWidths[2] / 2, yPos + 6, { align: 'center' }) + currentX += colWidths[2] + + pdf.text(salesData.summary.total_items.toString(), currentX + colWidths[3] / 2, yPos + 6, { align: 'center' }) + currentX += colWidths[3] + + pdf.text(this.formatCurrencyShort(salesData.summary.total_tax), currentX + colWidths[4] - 2, yPos + 6, { + align: 'right' + }) + currentX += colWidths[4] + + pdf.text(this.formatCurrencyShort(salesData.summary.total_discount), currentX + colWidths[5] - 2, yPos + 6, { + align: 'right' + }) + currentX += colWidths[5] + + pdf.text(this.formatCurrencyShort(salesData.summary.net_sales), currentX + colWidths[6] - 2, yPos + 6, { + align: 'right' + }) + + return yPos + 20 + } + + /** + * Format currency for display + */ + private static formatCurrency(amount: number): string { + return `Rp ${amount.toLocaleString('id-ID')}` + } + + /** + * Format currency short for table display + */ + private static formatCurrencyShort(amount: number): string { + if (amount >= 1000000) { + return `${(amount / 1000000).toFixed(1)}M` + } else if (amount >= 1000) { + return `${(amount / 1000).toFixed(0)}K` + } else { + return amount.toString() + } + } + + /** + * Format date for display + */ + private static formatDate(dateString: string): string { + const date = new Date(dateString) + return date.toLocaleDateString('id-ID', { + day: '2-digit', + month: '2-digit' + }) + } + + /** + * Generate filename with timestamp + */ + private static generateFilename(prefix: string, extension: string): string { + const now = new Date() + const year = now.getFullYear() + const month = (now.getMonth() + 1).toString().padStart(2, '0') + const day = now.getDate().toString().padStart(2, '0') + const hour = now.getHours().toString().padStart(2, '0') + const minute = now.getMinutes().toString().padStart(2, '0') + + return `${prefix}_${year}_${month}_${day}_${hour}${minute}.${extension}` + } + + /** + * Export Sales Order data with custom configuration + */ + static async exportCustomSalesOrderToPDF( + salesData: SalesReport, + options?: { + title?: string + includeSummary?: boolean + customFilename?: string + compactMode?: boolean + } + ) { + try { + const jsPDFModule = await import('jspdf') + const jsPDF = jsPDFModule.default + + const pdf = new jsPDF('p', 'mm', 'a4') + + let yPos = 20 + const pageWidth = pdf.internal.pageSize.getWidth() + const pageHeight = pdf.internal.pageSize.getHeight() + const marginLeft = 20 + const marginRight = 20 + const marginBottom = 15 + + const checkPageBreak = (neededSpace: number) => { + if (yPos + neededSpace > pageHeight - marginBottom) { + pdf.addPage() + yPos = 20 + return true + } + return false + } + + // Custom title if provided + if (options?.title) { + pdf.setFontSize(20) + pdf.setFont('helvetica', 'bold') + pdf.setTextColor(0, 0, 0) + pdf.text(options.title, pageWidth / 2, yPos, { align: 'center' }) + yPos += 15 + } else { + yPos = this.addReportTitle(pdf, salesData, yPos, pageWidth, marginLeft, marginRight) + } + + // Optional summary section + if (options?.includeSummary !== false) { + checkPageBreak(60) + yPos = this.addRingkasanSection(pdf, salesData, yPos, pageWidth, marginLeft, marginRight, checkPageBreak) + } + + // Daily sales details + checkPageBreak(100) + if (options?.compactMode) { + yPos = this.addCompactDailySalesSection( + pdf, + salesData, + yPos, + pageWidth, + marginLeft, + marginRight, + checkPageBreak + ) + } else { + yPos = this.addDailySalesSection(pdf, salesData, yPos, pageWidth, marginLeft, marginRight, checkPageBreak) + } + + const exportFilename = options?.customFilename || this.generateFilename('Custom_Sales_Order', 'pdf') + pdf.save(exportFilename) + + return { success: true, filename: exportFilename } + } catch (error) { + console.error('Error exporting custom sales order report to PDF:', error) + return { success: false, error: `PDF export failed: ${(error as Error).message}` } + } + } + + /** + * Add Compact Daily Sales section (fewer columns) + */ + private static addCompactDailySalesSection( + pdf: any, + salesData: SalesReport, + startY: number, + pageWidth: number, + marginLeft: number, + marginRight: number, + checkPageBreak: (space: number) => boolean + ): number { + let yPos = startY + + // Section title + pdf.setFontSize(14) + pdf.setFont('helvetica', 'bold') + pdf.setTextColor(102, 45, 145) + pdf.text('Rincian Harian (Compact)', marginLeft, yPos) + yPos += 12 + + // Reset formatting + pdf.setTextColor(0, 0, 0) + + // Table setup - compact with 4 columns + const tableWidth = pageWidth - marginLeft - marginRight + const colWidths = [30, 40, 30, 40] // Date, Sales, Orders, Net Sales + let currentX = marginLeft + + // Table header + pdf.setFillColor(240, 240, 240) + pdf.rect(marginLeft, yPos, tableWidth, 10, 'F') + + pdf.setFont('helvetica', 'bold') + pdf.setFontSize(9) + + const headers = ['Tanggal', 'Penjualan', 'Pesanan', 'Pendapatan'] + currentX = marginLeft + + headers.forEach((header, index) => { + if (index === 0) { + pdf.text(header, currentX + 2, yPos + 6) + } else { + pdf.text(header, currentX + colWidths[index] / 2, yPos + 6, { align: 'center' }) + } + currentX += colWidths[index] + }) + + yPos += 12 + + // Table rows + pdf.setFont('helvetica', 'normal') + pdf.setFontSize(9) + + salesData.data?.forEach((dailySales, index) => { + if (checkPageBreak(10)) yPos = 20 + + currentX = marginLeft + pdf.setFont('helvetica', 'normal') + pdf.setFontSize(9) + pdf.setTextColor(0, 0, 0) + + // Date + pdf.text(this.formatDate(dailySales.date), currentX + 2, yPos + 5) + currentX += colWidths[0] + + // Sales + pdf.text(this.formatCurrency(dailySales.sales), currentX + colWidths[1] - 2, yPos + 5, { align: 'right' }) + currentX += colWidths[1] + + // Orders + pdf.text(dailySales.orders.toString(), currentX + colWidths[2] / 2, yPos + 5, { align: 'center' }) + currentX += colWidths[2] + + // Net Sales + pdf.text(this.formatCurrency(dailySales.net_sales), currentX + colWidths[3] - 2, yPos + 5, { align: 'right' }) + + // Draw bottom border line + pdf.setDrawColor(230, 230, 230) + pdf.setLineWidth(0.3) + pdf.line(marginLeft, yPos + 8, marginLeft + tableWidth, yPos + 8) + + yPos += 10 + }) + + // Table footer (Total) + pdf.setFillColor(245, 245, 245) + pdf.rect(marginLeft, yPos, tableWidth, 10, 'F') + + pdf.setFont('helvetica', 'bold') + pdf.setFontSize(9) + + currentX = marginLeft + pdf.text('TOTAL', currentX + 2, yPos + 6) + currentX += colWidths[0] + + pdf.text(this.formatCurrency(salesData.summary.total_sales), currentX + colWidths[1] - 2, yPos + 6, { + align: 'right' + }) + currentX += colWidths[1] + + pdf.text(salesData.summary.total_orders.toString(), currentX + colWidths[2] / 2, yPos + 6, { align: 'center' }) + currentX += colWidths[2] + + pdf.text(this.formatCurrency(salesData.summary.net_sales), currentX + colWidths[3] - 2, yPos + 6, { + align: 'right' + }) + + return yPos + 20 + } +} diff --git a/src/views/apps/report/sales/sales-order/ReportSalesOrderContent.tsx b/src/views/apps/report/sales/sales-order/ReportSalesOrderContent.tsx index 20f91d4..6b447cb 100644 --- a/src/views/apps/report/sales/sales-order/ReportSalesOrderContent.tsx +++ b/src/views/apps/report/sales/sales-order/ReportSalesOrderContent.tsx @@ -4,19 +4,74 @@ import DateRangePicker from '@/components/RangeDatePicker' import { ReportItemHeader, ReportItemSubheader } from '@/components/report/ReportItem' import { useSalesAnalytics } from '@/services/queries/analytics' import { formatCurrency, formatDate, formatDateDDMMYYYY } from '@/utils/transform' -import { Button, Card, CardContent } from '@mui/material' +import { Button, Card, CardContent, Menu, MenuItem } from '@mui/material' import { useState } from 'react' import ReportSalesOrderCard from './ReportSalesOrderCard' +import { PDFExportSalesOrderService } from '@/services/export/pdf/PDFExportSalesOrderService' +import { ExcelExportSalesOrderService } from '@/services/export/excel/ExcelExportSalesOrderService' const ReportSalesOrderContent = () => { const [startDate, setStartDate] = useState(new Date()) const [endDate, setEndDate] = useState(new Date()) + const [anchorEl, setAnchorEl] = useState(null) const { data: sales } = useSalesAnalytics({ date_from: formatDateDDMMYYYY(startDate!), date_to: formatDateDDMMYYYY(endDate!) }) + const handleExportClick = (event: React.MouseEvent) => { + 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 ( <> @@ -28,11 +83,30 @@ const ReportSalesOrderContent = () => { color='secondary' variant='tonal' startIcon={} + endIcon={} className='max-sm:is-full' - // onClick={handleExportPDF} + onClick={handleExportClick} > Ekspor + + { + handleExportExcel() + handleExportClose() + }} + > + Export Excel + + { + handleExportPDF() + handleExportClose() + }} + > + Export PDF + +