diff --git a/package.json b/package.json index be06bb0..505258d 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "clsx": "^2.1.1", "cmdk": "^1.0.4", "date-fns": "^3.0.0", + "exceljs": "^4.4.0", "file-saver": "^2.0.5", "formik": "^2.4.6", "helmet": "^8.1.0", diff --git a/src/pages/wallet/wallet-statement/blocks/ShowDetailDialog.tsx b/src/pages/wallet/wallet-statement/blocks/ShowDetailDialog.tsx index 876d576..49dc5de 100644 --- a/src/pages/wallet/wallet-statement/blocks/ShowDetailDialog.tsx +++ b/src/pages/wallet/wallet-statement/blocks/ShowDetailDialog.tsx @@ -16,13 +16,8 @@ import { format } from 'date-fns'; import FilterSection from './FilterSection'; import * as XLSX from 'xlsx'; import jsPDF from 'jspdf'; -import 'jspdf-autotable'; - -interface AutoTableData { - pageNumber: number; - table: any; - doc: jsPDF; -} +import autoTable from 'jspdf-autotable'; +import ExcelJS from 'exceljs'; interface ContextProps { doExportData: (sorting: any, filter: any) => Promise; @@ -154,7 +149,7 @@ const ShowDetailWalletDialog = () => { enableSorting: false, cell: (info: any) => info.getValue() || '-', meta: { headerClassName: 'min-w-[200px]' } - } + } ]; const getTransactionLists = useCallback( @@ -238,15 +233,14 @@ const ShowDetailWalletDialog = () => { const formattedData = formatTransactionData(allData); const wb = XLSX.utils.book_new(); - const ws = XLSX.utils.json_to_sheet([]); + // Membuat space untuk header image (sekitar 10 baris) + const imageSpaceRows = 10; + const companyHeader = [ - ['T•PAY'], - ['Telin Digital Solution'], - ['Av. Pres. Nicolau Lobato, Dili'], - ['Tel: (+670) 74147147 / 147'], - ['www.t-pay.tl'], + // Baris kosong untuk space gambar header + ...Array(imageSpaceRows).fill(['']), [''], ['Wallet Statement Details'], [`Export Date: ${format(new Date(), 'dd MMM yyyy HH:mm:ss')}`], @@ -278,10 +272,11 @@ const ShowDetailWalletDialog = () => { XLSX.utils.sheet_add_aoa(ws, excelData); + // Set column widths const colWidths = [ { wch: 20 }, { wch: 15 }, - { wch: 25 }, + { wch: 35 }, { wch: 10 }, { wch: 12 }, { wch: 12 }, @@ -293,35 +288,18 @@ const ShowDetailWalletDialog = () => { ]; ws['!cols'] = colWidths; - const tpayCell = XLSX.utils.encode_cell({ r: 0, c: 0 }); - if (ws[tpayCell]) { - ws[tpayCell].s = { - font: { bold: true, sz: 18, color: { rgb: '000000' } }, - alignment: { horizontal: 'center' }, - fill: { fgColor: { rgb: 'F4C2C2' } } - }; - } - - for (let i = 1; i < 5; i++) { - const cellRef = XLSX.utils.encode_cell({ r: i, c: 0 }); - if (ws[cellRef]) { - ws[cellRef].s = { - font: { sz: 12, color: { rgb: '000000' } }, - alignment: { horizontal: 'center' }, - fill: { fgColor: { rgb: 'F4C2C2' } } - }; - } - } - - const titleCell = XLSX.utils.encode_cell({ r: 6, c: 0 }); + // Style untuk title "Wallet Statement Details" + const titleRowIndex = imageSpaceRows + 1; + const titleCell = XLSX.utils.encode_cell({ r: titleRowIndex, c: 0 }); if (ws[titleCell]) { ws[titleCell].s = { - font: { bold: true, sz: 14 }, + font: { bold: true, sz: 16 }, alignment: { horizontal: 'center' } }; } - for (let i = 7; i < 9; i++) { + // Style untuk export date dan wallet ID + for (let i = titleRowIndex + 1; i < titleRowIndex + 3; i++) { const cellRef = XLSX.utils.encode_cell({ r: i, c: 0 }); if (ws[cellRef]) { ws[cellRef].s = { @@ -331,14 +309,25 @@ const ShowDetailWalletDialog = () => { } } - for (let i = 0; i < 6; i++) { + // Merge cells untuk header area + if (!ws['!merges']) ws['!merges'] = []; + + // Merge untuk space gambar header + for (let i = 0; i < imageSpaceRows; i++) { const range = XLSX.utils.encode_range({ s: { r: i, c: 0 }, e: { r: i, c: 10 } }); - if (!ws['!merges']) ws['!merges'] = []; ws['!merges'].push(XLSX.utils.decode_range(range)); } + // Merge untuk title dan info + for (let i = titleRowIndex; i < titleRowIndex + 4; i++) { + const range = XLSX.utils.encode_range({ s: { r: i, c: 0 }, e: { r: i, c: 10 } }); + ws['!merges'].push(XLSX.utils.decode_range(range)); + } + + // Style untuk column headers + const headerRowIndex = titleRowIndex + 4; for (let col = 0; col < columnHeaders[0].length; col++) { - const cellRef = XLSX.utils.encode_cell({ r: 10, c: col }); + const cellRef = XLSX.utils.encode_cell({ r: headerRowIndex, c: col }); if (ws[cellRef]) { ws[cellRef].s = { font: { bold: true, color: { rgb: 'FFFFFF' } }, @@ -354,11 +343,14 @@ const ShowDetailWalletDialog = () => { } } - for (let row = 11; row < 11 + formattedData.length; row++) { + // Style untuk data rows + const dataStartRow = headerRowIndex + 1; + for (let row = dataStartRow; row < dataStartRow + formattedData.length; row++) { for (let col = 0; col < columnHeaders[0].length; col++) { const cellRef = XLSX.utils.encode_cell({ r: row, c: col }); if (ws[cellRef]) { ws[cellRef].s = { + alignment: { horizontal: 'left' }, // Semua kolom rata kiri border: { top: { style: 'thin', color: { rgb: 'D9D9D9' } }, bottom: { style: 'thin', color: { rgb: 'D9D9D9' } }, @@ -366,156 +358,257 @@ const ShowDetailWalletDialog = () => { right: { style: 'thin', color: { rgb: 'D9D9D9' } } } }; - - if ([4, 5, 6].includes(col)) { - ws[cellRef].s.alignment = { horizontal: 'right' }; - } } } } - XLSX.utils.book_append_sheet(wb, ws, 'Wallet Statement'); + try { + // Import ExcelJS untuk menambahkan gambar + const ExcelJS = await import('exceljs'); - const fileName = `wallet_statement_${selectedWallet?.ID}_${format(new Date(), 'yyyyMMdd_HHmmss')}.xlsx`; - XLSX.writeFile(wb, fileName); + // Buat workbook baru dengan ExcelJS + const workbook = new ExcelJS.Workbook(); + const worksheet = workbook.addWorksheet('Wallet Statement'); - // console.log('Excel export completed successfully'); + // Tambahkan data dari XLSX worksheet ke ExcelJS worksheet + const range = XLSX.utils.decode_range(ws['!ref'] || 'A1:K1'); + + for (let row = range.s.r; row <= range.e.r; row++) { + for (let col = range.s.c; col <= range.e.c; col++) { + const cellAddress = XLSX.utils.encode_cell({ r: row, c: col }); + const cell = ws[cellAddress]; + if (cell && cell.v !== undefined) { + const excelCell = worksheet.getCell(row + 1, col + 1); + excelCell.value = cell.v; + + // Apply styles + if (cell.s) { + if (cell.s.font) { + excelCell.font = { + bold: cell.s.font.bold || false, + size: cell.s.font.sz || 11, + color: cell.s.font.color ? { argb: cell.s.font.color.rgb } : undefined + }; + } + if (cell.s.fill) { + excelCell.fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: cell.s.fill.fgColor.rgb } + }; + } + if (cell.s.alignment) { + excelCell.alignment = { + horizontal: cell.s.alignment.horizontal || 'left' + }; + } + if (cell.s.border) { + excelCell.border = { + top: cell.s.border.top ? { style: 'thin' } : undefined, + bottom: cell.s.border.bottom ? { style: 'thin' } : undefined, + left: cell.s.border.left ? { style: 'thin' } : undefined, + right: cell.s.border.right ? { style: 'thin' } : undefined + }; + } + } + } + } + } + + // Set column widths + colWidths.forEach((width, index) => { + worksheet.getColumn(index + 1).width = width.wch; + }); + + // Merge cells + if (ws['!merges']) { + ws['!merges'].forEach((merge) => { + worksheet.mergeCells(merge.s.r + 1, merge.s.c + 1, merge.e.r + 1, merge.e.c + 1); + }); + } + + // Tambahkan gambar header + try { + const response = await fetch('/media/avatars/KopDetailWalletStatement.png'); + if (response.ok) { + const imageBuffer = await response.arrayBuffer(); + + const imageId = workbook.addImage({ + buffer: imageBuffer, + extension: 'png' + }); + + // Posisikan gambar di area header + worksheet.addImage(imageId, { + tl: { col: 1.7, row: 1 }, + ext: { width: 1100, height: 180 } + }); + } + } catch (imageError) { + console.warn('Could not load header image, continuing without it:', imageError); + } + + // Export file + const fileName = `wallet_statement_${selectedWallet?.ID}_${format(new Date(), 'yyyyMMdd_HHmmss')}.xlsx`; + const buffer = await workbook.xlsx.writeBuffer(); + + // Download file + const blob = new Blob([buffer], { + type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + }); + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = fileName; + link.click(); + window.URL.revokeObjectURL(url); + } catch (excelJSError) { + console.warn('ExcelJS not available, falling back to XLSX without image:', excelJSError); + + XLSX.utils.book_append_sheet(wb, ws, 'Wallet Statement'); + const fileName = `wallet_statement_${selectedWallet?.ID}_${format(new Date(), 'yyyyMMdd_HHmmss')}.xlsx`; + XLSX.writeFile(wb, fileName); + } } catch (error) { console.error('Error exporting to Excel:', error); + toast.error('Failed to export Excel'); } }; - // Perbaikan untuk fungsi doExportPDF -const doExportPDF = async () => { - try { - // Import jsPDF dengan cara yang benar - const jsPDF = (await import('jspdf')).default; - - // Import autoTable plugin - const autoTable = (await import('jspdf-autotable')).default; - - const allData = await getAllTransactionData(); - const formattedData = formatTransactionData(allData); + const doExportPDF = async () => { + try { + const allData = await getAllTransactionData(); + const formattedData = formatTransactionData(allData); - // Buat instance jsPDF - const pdf = new jsPDF({ - orientation: 'landscape', // Ubah ke landscape untuk tabel yang lebar - unit: 'mm', - format: 'a4' - }); + const pdf = new jsPDF({ + orientation: 'landscape', + unit: 'mm', + format: 'a4' + }); - // Header perusahaan - pdf.setFontSize(18); - pdf.setFont('helvetica', 'bold'); - pdf.text('T•PAY', pdf.internal.pageSize.getWidth() / 2, 20, { align: 'center' }); - - pdf.setFontSize(12); - pdf.setFont('helvetica', 'normal'); - pdf.text('Telin Digital Solution', pdf.internal.pageSize.getWidth() / 2, 28, { align: 'center' }); - pdf.text('Av. Pres. Nicolau Lobato, Dili', pdf.internal.pageSize.getWidth() / 2, 34, { align: 'center' }); - pdf.text('Tel: (+670) 74147147 / 147', pdf.internal.pageSize.getWidth() / 2, 40, { align: 'center' }); - pdf.text('www.t-pay.tl', pdf.internal.pageSize.getWidth() / 2, 46, { align: 'center' }); + const headerImageUrl = '/media/avatars/KopDetailWalletStatement.png'; - // Garis pemisah - pdf.setDrawColor(0, 0, 0); - pdf.setLineWidth(0.5); - pdf.line(20, 52, pdf.internal.pageSize.getWidth() - 20, 52); + try { + const pageWidth = pdf.internal.pageSize.getWidth(); + const imageWidth = 250; + const imageHeight = 40; + const xPosition = (pageWidth - imageWidth) / 2; - // Judul laporan - pdf.setFontSize(16); - pdf.setFont('helvetica', 'bold'); - pdf.text('Wallet Statement Details', pdf.internal.pageSize.getWidth() / 2, 62, { align: 'center' }); + pdf.addImage(headerImageUrl, 'PNG', xPosition, 10, imageWidth, imageHeight); - // Informasi tambahan - pdf.setFontSize(10); - pdf.setFont('helvetica', 'normal'); - pdf.text(`Export Date: ${format(new Date(), 'dd MMM yyyy HH:mm:ss')}`, 20, 72); - pdf.text(`Wallet ID: ${selectedWallet?.ID || '-'}`, 20, 78); + let currentY = 55; - // Siapkan data tabel - const tableHeaders = [ - 'Transaction Code', - 'MSISDN Reffer', - 'Transaction Type', - 'Type', - 'Pre Amount', - 'Amount', - 'Post Amount', - 'Category', - 'Date', - 'Purpose', - 'Notes' - ]; + pdf.setDrawColor(0, 0, 0); + pdf.setLineWidth(0.5); + pdf.line(20, currentY, pageWidth - 20, currentY); - const tableData = formattedData.map((item) => [ - item['Transaction Code'], - item['MSISDN Reffer'], - item['Transaction Type'], - item['Type'], - item['Pre Amount'], - item['Amount'], - item['Post Amount'], - item['Category'], - item['Date'], - item['Purpose'], - item['Notes'] - ]); + currentY += 10; - // Gunakan autoTable dengan cara yang benar - autoTable(pdf, { - head: [tableHeaders], - body: tableData, - startY: 85, - styles: { - fontSize: 7, - cellPadding: 2, - overflow: 'linebreak', - halign: 'center' - }, - headStyles: { - fillColor: [68, 114, 196], - textColor: [255, 255, 255], - fontStyle: 'bold', - halign: 'center' - }, - columnStyles: { - 0: { cellWidth: 25, halign: 'left' }, // Transaction Code - 1: { cellWidth: 20, halign: 'left' }, // MSISDN Reffer - 2: { cellWidth: 25, halign: 'left' }, // Transaction Type - 3: { cellWidth: 15, halign: 'center' }, // Type - 4: { cellWidth: 18, halign: 'right' }, // Pre Amount - 5: { cellWidth: 18, halign: 'right' }, // Amount - 6: { cellWidth: 18, halign: 'right' }, // Post Amount - 7: { cellWidth: 20, halign: 'left' }, // Category - 8: { cellWidth: 30, halign: 'left' }, // Date - 9: { cellWidth: 25, halign: 'left' }, // Purpose - 10: { cellWidth: 25, halign: 'left' } // Notes - }, - margin: { top: 85, left: 10, right: 10 }, - tableWidth: 'auto', - // Callback untuk styling baris - didDrawCell: (data: any) => { - // Tambahkan border pada setiap cell - if (data.section === 'body') { - pdf.setDrawColor(200, 200, 200); - pdf.setLineWidth(0.1); - } + pdf.setFontSize(16); + pdf.setFont('helvetica', 'bold'); + pdf.text('Wallet Statement Details', pageWidth / 2, currentY, { align: 'center' }); + + currentY += 10; + + pdf.setFontSize(10); + pdf.setFont('helvetica', 'normal'); + pdf.text(`Export Date: ${format(new Date(), 'dd MMM yyyy HH:mm:ss')}`, 20, currentY); + pdf.text(`Wallet ID: ${selectedWallet?.ID || '-'}`, 20, currentY + 6); + + currentY += 20; + + const tableHeaders = [ + 'Transaction Code', + 'MSISDN Reffer', + 'Transaction Type', + 'Type', + 'Pre Amount', + 'Amount', + 'Post Amount', + 'Category', + 'Date', + 'Purpose', + 'Notes' + ]; + + const tableData = formattedData.map((item) => [ + item['Transaction Code'], + item['MSISDN Reffer'], + item['Transaction Type'], + item['Type'], + item['Pre Amount'], + item['Amount'], + item['Post Amount'], + item['Category'], + item['Date'], + item['Purpose'], + item['Notes'] + ]); + + const calculateLeftMargin = ( + columnStyles: Record, + pageWidth: number + ): number => { + const totalTableWidth = Object.values(columnStyles).reduce( + (sum, col) => sum + col.cellWidth, + 0 + ); + + return (pageWidth - totalTableWidth) / 2; + }; + const columnStyles = { + 0: { cellWidth: 25, halign: 'left' as const }, + 1: { cellWidth: 20, halign: 'left' as const }, + 2: { cellWidth: 25, halign: 'left' as const }, + 3: { cellWidth: 15, halign: 'center' as const }, + 4: { cellWidth: 18, halign: 'right' as const }, + 5: { cellWidth: 18, halign: 'right' as const }, + 6: { cellWidth: 18, halign: 'right' as const }, + 7: { cellWidth: 20, halign: 'left' as const }, + 8: { cellWidth: 30, halign: 'left' as const }, + 9: { cellWidth: 25, halign: 'left' as const }, + 10: { cellWidth: 25, halign: 'left' as const } + }; + + const leftMargin = calculateLeftMargin(columnStyles, pageWidth); + + autoTable(pdf, { + head: [tableHeaders], + body: tableData, + startY: currentY, + styles: { + fontSize: 7, + cellPadding: 2, + overflow: 'linebreak', + halign: 'center' + }, + headStyles: { + fillColor: [68, 114, 196], + textColor: [255, 255, 255], + fontStyle: 'bold', + halign: 'center' + }, + columnStyles, + tableWidth: 'wrap', + margin: { top: currentY, left: leftMargin }, + didDrawCell: (data: any) => { + if (data.section === 'body') { + pdf.setDrawColor(200, 200, 200); + pdf.setLineWidth(0.1); + } + } + }); + + const fileName = `wallet_statement_${selectedWallet?.ID}_${format(new Date(), 'yyyyMMdd_HHmmss')}.pdf`; + pdf.save(fileName); + } catch (imageError) { + console.error('Error loading header image:', imageError); } - }); - - // Generate nama file - const fileName = `wallet_statement_${selectedWallet?.ID}_${format(new Date(), 'yyyyMMdd_HHmmss')}.pdf`; - - // Save file - pdf.save(fileName); - - toast.success('PDF exported successfully'); - - } catch (error) { + } catch (error) { console.error('Error exporting to PDF:', error); toast.error('Failed to export PDF'); } -}; + }; const doExportData = async (sorting: any, filter: any) => { const user = localStorage.getItem( @@ -599,4 +692,4 @@ const doExportPDF = async () => { ); }; -export default ShowDetailWalletDialog; +export default ShowDetailWalletDialog; \ No newline at end of file