This commit is contained in:
Raja Oktafrianto
2025-06-06 15:22:54 +07:00
parent 3fa9764340
commit ad0af0e332

View File

@ -16,14 +16,9 @@ import { format } from 'date-fns';
import FilterSection from './FilterSection'; import FilterSection from './FilterSection';
import * as XLSX from 'xlsx'; import * as XLSX from 'xlsx';
import jsPDF from 'jspdf'; import jsPDF from 'jspdf';
import autoTable from 'jspdf-autotable';
import ExcelJS from 'exceljs'; import ExcelJS from 'exceljs';
let autoTable: any; import { toAbsoluteUrl } from '@/utils';
try {
// Method 1: Default import
autoTable = require('jspdf-autotable').default;
} catch (e1) {
console.error('Failed to import jspdf-autotable:', e1);
}
interface ContextProps { interface ContextProps {
doExportData: (sorting: any, filter: any) => Promise<any>; doExportData: (sorting: any, filter: any) => Promise<any>;
@ -233,15 +228,6 @@ const ShowDetailWalletDialog = () => {
})); }));
}; };
const isImageAccessible = async (url: any) => {
try {
const response = await fetch(url, { method: 'HEAD' });
return response.ok;
} catch {
return false;
}
};
const doExportExcel = async () => { const doExportExcel = async () => {
try { try {
const allData = await getAllTransactionData(); const allData = await getAllTransactionData();
@ -250,9 +236,11 @@ const ShowDetailWalletDialog = () => {
const wb = XLSX.utils.book_new(); const wb = XLSX.utils.book_new();
const ws = XLSX.utils.json_to_sheet([]); const ws = XLSX.utils.json_to_sheet([]);
// Space untuk header image // Membuat space untuk header image (sekitar 10 baris)
const imageSpaceRows = 10; const imageSpaceRows = 10;
const companyHeader = [ const companyHeader = [
// Baris kosong untuk space gambar header
...Array(imageSpaceRows).fill(['']), ...Array(imageSpaceRows).fill(['']),
[''], [''],
['Wallet Statement Details'], ['Wallet Statement Details'],
@ -263,8 +251,17 @@ const ShowDetailWalletDialog = () => {
const columnHeaders = [ const columnHeaders = [
[ [
'Transaction Code', 'MSISDN Reffer', 'Transaction Type', 'Type', 'Transaction Code',
'Pre Amount', 'Amount', 'Post Amount', 'Category', 'Date', 'Purpose', 'Notes' 'MSISDN Reffer',
'Transaction Type',
'Type',
'Pre Amount',
'Amount',
'Post Amount',
'Category',
'Date',
'Purpose',
'Notes'
] ]
]; ];
@ -278,12 +275,21 @@ const ShowDetailWalletDialog = () => {
// Set column widths // Set column widths
const colWidths = [ const colWidths = [
{ wch: 20 }, { wch: 15 }, { wch: 35 }, { wch: 10 }, { wch: 12 }, { wch: 20 },
{ wch: 12 }, { wch: 12 }, { wch: 15 }, { wch: 20 }, { wch: 25 }, { wch: 25 } { wch: 15 },
{ wch: 35 },
{ wch: 10 },
{ wch: 12 },
{ wch: 12 },
{ wch: 12 },
{ wch: 15 },
{ wch: 20 },
{ wch: 25 },
{ wch: 25 }
]; ];
ws['!cols'] = colWidths; ws['!cols'] = colWidths;
// Apply styles // Style untuk title "Wallet Statement Details"
const titleRowIndex = imageSpaceRows + 1; const titleRowIndex = imageSpaceRows + 1;
const titleCell = XLSX.utils.encode_cell({ r: titleRowIndex, c: 0 }); const titleCell = XLSX.utils.encode_cell({ r: titleRowIndex, c: 0 });
if (ws[titleCell]) { if (ws[titleCell]) {
@ -293,19 +299,33 @@ const ShowDetailWalletDialog = () => {
}; };
} }
// Merge cells // 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 = {
font: { sz: 11 },
alignment: { horizontal: 'left' }
};
}
}
// Merge cells untuk header area
if (!ws['!merges']) ws['!merges'] = []; if (!ws['!merges']) ws['!merges'] = [];
// Merge untuk space gambar header
for (let i = 0; i < imageSpaceRows; i++) { for (let i = 0; i < imageSpaceRows; i++) {
const range = XLSX.utils.encode_range({ s: { r: i, c: 0 }, e: { r: i, c: 10 } }); const range = XLSX.utils.encode_range({ s: { r: i, c: 0 }, e: { r: i, c: 10 } });
ws['!merges'].push(XLSX.utils.decode_range(range)); ws['!merges'].push(XLSX.utils.decode_range(range));
} }
// Merge untuk title dan info
for (let i = titleRowIndex; i < titleRowIndex + 4; i++) { for (let i = titleRowIndex; i < titleRowIndex + 4; i++) {
const range = XLSX.utils.encode_range({ s: { r: i, c: 0 }, e: { r: i, c: 10 } }); const range = XLSX.utils.encode_range({ s: { r: i, c: 0 }, e: { r: i, c: 10 } });
ws['!merges'].push(XLSX.utils.decode_range(range)); ws['!merges'].push(XLSX.utils.decode_range(range));
} }
// Style column headers // Style untuk column headers
const headerRowIndex = titleRowIndex + 4; const headerRowIndex = titleRowIndex + 4;
for (let col = 0; col < columnHeaders[0].length; col++) { for (let col = 0; col < columnHeaders[0].length; col++) {
const cellRef = XLSX.utils.encode_cell({ r: headerRowIndex, c: col }); const cellRef = XLSX.utils.encode_cell({ r: headerRowIndex, c: col });
@ -324,14 +344,14 @@ const ShowDetailWalletDialog = () => {
} }
} }
// Style data rows // Style untuk data rows
const dataStartRow = headerRowIndex + 1; const dataStartRow = headerRowIndex + 1;
for (let row = dataStartRow; row < dataStartRow + formattedData.length; row++) { for (let row = dataStartRow; row < dataStartRow + formattedData.length; row++) {
for (let col = 0; col < columnHeaders[0].length; col++) { for (let col = 0; col < columnHeaders[0].length; col++) {
const cellRef = XLSX.utils.encode_cell({ r: row, c: col }); const cellRef = XLSX.utils.encode_cell({ r: row, c: col });
if (ws[cellRef]) { if (ws[cellRef]) {
ws[cellRef].s = { ws[cellRef].s = {
alignment: { horizontal: 'left' }, alignment: { horizontal: 'left' }, // Semua kolom rata kiri
border: { border: {
top: { style: 'thin', color: { rgb: 'D9D9D9' } }, top: { style: 'thin', color: { rgb: 'D9D9D9' } },
bottom: { style: 'thin', color: { rgb: 'D9D9D9' } }, bottom: { style: 'thin', color: { rgb: 'D9D9D9' } },
@ -343,21 +363,17 @@ const ShowDetailWalletDialog = () => {
} }
} }
// Try ExcelJS with better error handling
let useExcelJS = false;
try { try {
// Check if ExcelJS is available // Import ExcelJS untuk menambahkan gambar
const ExcelJSModule = await import('exceljs'); const ExcelJS = await import('exceljs');
const ExcelJS = ExcelJSModule.default || ExcelJSModule;
if (ExcelJS && ExcelJS.Workbook) {
useExcelJS = true;
// Buat workbook baru dengan ExcelJS
const workbook = new ExcelJS.Workbook(); const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet('Wallet Statement'); const worksheet = workbook.addWorksheet('Wallet Statement');
// Transfer data from XLSX to ExcelJS // Tambahkan data dari XLSX worksheet ke ExcelJS worksheet
const range = XLSX.utils.decode_range(ws['!ref'] || 'A1:K1'); const range = XLSX.utils.decode_range(ws['!ref'] || 'A1:K1');
for (let row = range.s.r; row <= range.e.r; row++) { for (let row = range.s.r; row <= range.e.r; row++) {
for (let col = range.s.c; col <= range.e.c; col++) { for (let col = range.s.c; col <= range.e.c; col++) {
const cellAddress = XLSX.utils.encode_cell({ r: row, c: col }); const cellAddress = XLSX.utils.encode_cell({ r: row, c: col });
@ -412,46 +428,34 @@ const ShowDetailWalletDialog = () => {
}); });
} }
// Try to add header image dengan multiple fallback paths // Tambahkan gambar header
const imagePaths = [
'/media/avatars/KopDetailWalletStatement.png',
'./media/avatars/KopDetailWalletStatement.png',
];
let imageAdded = false;
for (const imagePath of imagePaths) {
try { try {
if (await isImageAccessible(imagePath)) { const response = await fetch(
const response = await fetch(imagePath); toAbsoluteUrl('/media/avatars/KopDetailWalletStatement.png')
);
if (response.ok) { if (response.ok) {
const imageBuffer = await response.arrayBuffer(); const imageBuffer = await response.arrayBuffer();
const imageId = workbook.addImage({ const imageId = workbook.addImage({
buffer: imageBuffer, buffer: imageBuffer,
extension: 'png' extension: 'png'
}); });
// Posisikan gambar di area header
worksheet.addImage(imageId, { worksheet.addImage(imageId, {
tl: { col: 1.7, row: 1 }, tl: { col: 1.7, row: 1 },
ext: { width: 1100, height: 180 } ext: { width: 1100, height: 180 }
}); });
imageAdded = true;
break;
}
} }
} catch (imageError) { } catch (imageError) {
console.warn(`Failed to load image from ${imagePath}:`, imageError); console.warn('Could not load header image, continuing without it:', imageError);
continue;
}
}
if (!imageAdded) {
console.warn('No header image could be loaded from any path');
} }
// Export file // Export file
const fileName = `wallet_statement_${selectedWallet?.ID}_${format(new Date(), 'yyyyMMdd_HHmmss')}.xlsx`; const fileName = `wallet_statement_${selectedWallet?.ID}_${format(new Date(), 'yyyyMMdd_HHmmss')}.xlsx`;
const buffer = await workbook.xlsx.writeBuffer(); const buffer = await workbook.xlsx.writeBuffer();
// Download file
const blob = new Blob([buffer], { const blob = new Blob([buffer], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
}); });
@ -461,47 +465,21 @@ const ShowDetailWalletDialog = () => {
link.download = fileName; link.download = fileName;
link.click(); link.click();
window.URL.revokeObjectURL(url); window.URL.revokeObjectURL(url);
}
} catch (excelJSError) { } catch (excelJSError) {
console.warn('ExcelJS failed, falling back to XLSX:', excelJSError); console.warn('ExcelJS not available, falling back to XLSX without image:', excelJSError);
useExcelJS = false;
}
// Fallback to XLSX if ExcelJS failed
if (!useExcelJS) {
XLSX.utils.book_append_sheet(wb, ws, 'Wallet Statement'); XLSX.utils.book_append_sheet(wb, ws, 'Wallet Statement');
const fileName = `wallet_statement_${selectedWallet?.ID}_${format(new Date(), 'yyyyMMdd_HHmmss')}.xlsx`; const fileName = `wallet_statement_${selectedWallet?.ID}_${format(new Date(), 'yyyyMMdd_HHmmss')}.xlsx`;
XLSX.writeFile(wb, fileName); XLSX.writeFile(wb, fileName);
} }
} catch (error) {
toast.success('Excel export completed successfully');
} catch (error: any) {
console.error('Error exporting to Excel:', error); console.error('Error exporting to Excel:', error);
toast.error('Failed to export Excel: ' + error.message); toast.error('Failed to export Excel');
} }
}; };
const doExportPDF = async () => { const doExportPDF = async () => {
try { try {
// Check if autoTable is available
if (!autoTable) {
// Try to load autoTable dynamically
try {
const autoTableModule = await import('jspdf-autotable');
autoTable = autoTableModule.default || autoTableModule.autoTable || autoTableModule;
} catch (importError) {
console.error('Failed to import jspdf-autotable:', importError);
toast.error('PDF export library not available');
return;
}
}
if (typeof autoTable !== 'function') {
console.error('autoTable is not a function, type:', typeof autoTable);
toast.error('PDF export function not properly loaded');
return;
}
const allData = await getAllTransactionData(); const allData = await getAllTransactionData();
const formattedData = formatTransactionData(allData); const formattedData = formatTransactionData(allData);
@ -511,84 +489,92 @@ const ShowDetailWalletDialog = () => {
format: 'a4' format: 'a4'
}); });
// Try to add header image dengan multiple paths const headerImageUrl = toAbsoluteUrl('/media/avatars/KopDetailWalletStatement.png');
const imagePaths = [
'/media/avatars/KopDetailWalletStatement.png',
'./media/avatars/KopDetailWalletStatement.png',
];
let imageAdded = false;
for (const imagePath of imagePaths) {
try { try {
if (await isImageAccessible(imagePath)) {
const pageWidth = pdf.internal.pageSize.getWidth(); const pageWidth = pdf.internal.pageSize.getWidth();
const imageWidth = 250; const imageWidth = 250;
const imageHeight = 40; const imageHeight = 40;
const xPosition = (pageWidth - imageWidth) / 2; const xPosition = (pageWidth - imageWidth) / 2;
pdf.addImage(imagePath, 'PNG', xPosition, 10, imageWidth, imageHeight); pdf.addImage(headerImageUrl, 'PNG', xPosition, 10, imageWidth, imageHeight);
imageAdded = true;
break;
}
} catch (imageError) {
console.warn(`Failed to load image from ${imagePath}:`, imageError);
continue;
}
}
const pageWidth = pdf.internal.pageSize.getWidth(); let currentY = 55;
let currentY = imageAdded ? 55 : 20;
// Add header line
pdf.setDrawColor(0, 0, 0); pdf.setDrawColor(0, 0, 0);
pdf.setLineWidth(0.5); pdf.setLineWidth(0.5);
pdf.line(20, currentY, pageWidth - 20, currentY); pdf.line(20, currentY, pageWidth - 20, currentY);
currentY += 10; currentY += 10;
// Add title
pdf.setFontSize(16); pdf.setFontSize(16);
pdf.setFont('helvetica', 'bold'); pdf.setFont('helvetica', 'bold');
pdf.text('Wallet Statement Details', pageWidth / 2, currentY, { align: 'center' }); pdf.text('Wallet Statement Details', pageWidth / 2, currentY, { align: 'center' });
currentY += 10; currentY += 10;
// Add metadata
pdf.setFontSize(10); pdf.setFontSize(10);
pdf.setFont('helvetica', 'normal'); pdf.setFont('helvetica', 'normal');
pdf.text(`Export Date: ${format(new Date(), 'dd MMM yyyy HH:mm:ss')}`, 20, currentY); pdf.text(`Export Date: ${format(new Date(), 'dd MMM yyyy HH:mm:ss')}`, 20, currentY);
pdf.text(`Wallet ID: ${selectedWallet?.ID || '-'}`, 20, currentY + 6); pdf.text(`Wallet ID: ${selectedWallet?.ID || '-'}`, 20, currentY + 6);
currentY += 20; currentY += 20;
// Prepare table data
const tableHeaders = [ const tableHeaders = [
'Transaction Code', 'MSISDN Reffer', 'Transaction Type', 'Type', 'Transaction Code',
'Pre Amount', 'Amount', 'Post Amount', 'Category', 'Date', 'Purpose', 'Notes' 'MSISDN Reffer',
'Transaction Type',
'Type',
'Pre Amount',
'Amount',
'Post Amount',
'Category',
'Date',
'Purpose',
'Notes'
]; ];
const tableData = formattedData.map((item) => [ const tableData = formattedData.map((item) => [
item['Transaction Code'], item['MSISDN Reffer'], item['Transaction Type'], item['Type'], item['Transaction Code'],
item['Pre Amount'], item['Amount'], item['Post Amount'], item['Category'], item['MSISDN Reffer'],
item['Date'], item['Purpose'], item['Notes'] 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<number, { cellWidth: number }>,
pageWidth: number
): number => {
const totalTableWidth = Object.values(columnStyles).reduce(
(sum, col) => sum + col.cellWidth,
0
);
return (pageWidth - totalTableWidth) / 2;
};
const columnStyles = { const columnStyles = {
0: { cellWidth: 25, halign: 'left' }, 0: { cellWidth: 25, halign: 'left' as const },
1: { cellWidth: 20, halign: 'left' }, 1: { cellWidth: 20, halign: 'left' as const },
2: { cellWidth: 25, halign: 'left' }, 2: { cellWidth: 25, halign: 'left' as const },
3: { cellWidth: 15, halign: 'center' }, 3: { cellWidth: 15, halign: 'center' as const },
4: { cellWidth: 18, halign: 'right' }, 4: { cellWidth: 18, halign: 'right' as const },
5: { cellWidth: 18, halign: 'right' }, 5: { cellWidth: 18, halign: 'right' as const },
6: { cellWidth: 18, halign: 'right' }, 6: { cellWidth: 18, halign: 'right' as const },
7: { cellWidth: 20, halign: 'left' }, 7: { cellWidth: 20, halign: 'left' as const },
8: { cellWidth: 30, halign: 'left' }, 8: { cellWidth: 30, halign: 'left' as const },
9: { cellWidth: 25, halign: 'left' }, 9: { cellWidth: 25, halign: 'left' as const },
10: { cellWidth: 25, halign: 'left' } 10: { cellWidth: 25, halign: 'left' as const }
}; };
const totalTableWidth = Object.values(columnStyles).reduce((sum, col) => sum + col.cellWidth, 0); const leftMargin = calculateLeftMargin(columnStyles, pageWidth);
const leftMargin = (pageWidth - totalTableWidth) / 2;
// Use autoTable with proper error handling
try {
autoTable(pdf, { autoTable(pdf, {
head: [tableHeaders], head: [tableHeaders],
body: tableData, body: tableData,
@ -615,19 +601,15 @@ const ShowDetailWalletDialog = () => {
} }
} }
}); });
} catch (autoTableError) {
console.error('AutoTable error:', autoTableError);
toast.error('Failed to generate PDF table');
return;
}
const fileName = `wallet_statement_${selectedWallet?.ID}_${format(new Date(), 'yyyyMMdd_HHmmss')}.pdf`; const fileName = `wallet_statement_${selectedWallet?.ID}_${format(new Date(), 'yyyyMMdd_HHmmss')}.pdf`;
pdf.save(fileName); pdf.save(fileName);
} catch (imageError) {
toast.success('PDF export completed successfully'); console.error('Error loading header image:', imageError);
} catch (error: any) { }
} catch (error) {
console.error('Error exporting to PDF:', error); console.error('Error exporting to PDF:', error);
toast.error('Failed to export PDF: ' + error.message); toast.error('Failed to export PDF');
} }
}; };