Merge remote-tracking branch 'production/radja-dev'

This commit is contained in:
Raja Oktafrianto
2025-06-05 20:09:22 +07:00
10 changed files with 506 additions and 75 deletions

View File

@ -1,4 +1,4 @@
import React, { useCallback, useState, useEffect } from 'react';
import React, { useCallback, useState, useEffect, useContext } from 'react';
import { Button } from '@/components/ui/button';
import {
Select,
@ -11,6 +11,7 @@ import { DefaultTooltip, KeenIcon } from '@/components';
import { toAbsoluteUrl } from '@/utils';
import { useCallApi } from '@/hooks';
import { apiConfig } from '@/config/api.config';
import { showDetailWalletContext } from './ShowDetailDialog';
interface TransferType {
id: string;
@ -40,24 +41,44 @@ const FilterSection: React.FC<FilterSectionProps> = ({
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
const [category, setCategory] = useState<string[]>([]);
const [searchValue, setSearchValue] = useState('');
const { doExportExcel, doExportPDF } = useContext(showDetailWalletContext);
const handleExportExcel = async () => {
try {
await doExportExcel();
// console.log('Excel export initiated successfully');
} catch (error) {
console.error('Error during Excel export:', error);
}
};
const handleExportPDF = async () => {
try {
await doExportPDF();
// console.log('PDF export initiated successfully');
} catch (error) {
console.error('Error during PDF export:', error);
}
};
const fetchTransferType = useCallback(async () => {
try {
const response = await GetData(`${apiConfig.service_transaction}/transactiontype/list`, {
limit: 100,
page: 1,
with_deleted: false,
order_field: 'created_at',
order_direction: 'ASC'
});
const sortedData = (response?.data?.list || [])
.sort((a: any, b: any) => a.name.localeCompare(b.name));
setTransferType(sortedData);
} catch (error) {
console.error('Error fetching transfer types', error);
}
}, [GetData]);
try {
const response = await GetData(`${apiConfig.service_transaction}/transactiontype/list`, {
limit: 100,
page: 1,
with_deleted: false,
order_field: 'created_at',
order_direction: 'ASC'
});
const sortedData = (response?.data?.list || []).sort((a: any, b: any) =>
a.name.localeCompare(b.name)
);
setTransferType(sortedData);
} catch (error) {
console.error('Error fetching transfer types', error);
}
}, [GetData]);
useEffect(() => {
fetchTransferType();
@ -70,8 +91,8 @@ const FilterSection: React.FC<FilterSectionProps> = ({
);
useEffect(() => {
setCategory(categories);
}, [transaction, categoryMap]);
setCategory(categories);
}, [transaction, categoryMap]);
const reverseCategoryMap = Object.fromEntries(
Object.entries(categoryMap).map(([key, value]) => [value, key])
@ -108,7 +129,7 @@ const FilterSection: React.FC<FilterSectionProps> = ({
};
}
if (selectedTransferType) {
if (selectedTransferType) {
appliedFilters.id_transaction_type = selectedTransferType;
}
@ -200,20 +221,22 @@ const FilterSection: React.FC<FilterSectionProps> = ({
Filter Data
</Button>
<DefaultTooltip title={'Export Data'} placement={'top'}>
<Button variant={'outline'} className="h-7.5 min-w-[58px]">
<img src={toAbsoluteUrl('/media/file-types/xls.svg')} className="" alt="" />
</Button>
</DefaultTooltip>
<div className="flex gap-2">
<DefaultTooltip title={'Export to Excel'} placement={'top'}>
<Button variant={'outline'} className="h-7.5 min-w-[58px]" onClick={handleExportExcel}>
<img src={toAbsoluteUrl('/media/file-types/xls.svg')} className="" alt="Excel" />
</Button>
</DefaultTooltip>
<DefaultTooltip title={'Export Data'} placement={'top'}>
<Button variant={'outline'} className="h-7.5 min-w-[58px]">
<img src={toAbsoluteUrl('/media/file-types/pdf.svg')} className="" alt="" />
</Button>
</DefaultTooltip>
<DefaultTooltip title={'Export to PDF'} placement={'top'}>
<Button variant={'outline'} className="h-7.5 min-w-[58px]" onClick={handleExportPDF}>
<img src={toAbsoluteUrl('/media/file-types/pdf.svg')} className="" alt="PDF" />
</Button>
</DefaultTooltip>
</div>
</div>
</div>
);
};
export default FilterSection;
export default FilterSection;

View File

@ -11,16 +11,29 @@ import { useCallApi } from '@/hooks';
import { apiConfig } from '@/config/api.config';
import { DataGridColumnHeader, DataGridProvider } from '@/components';
import { useManageStatementContext } from '../hooks/useManageWalletStatementContext';
import { Toaster } from 'sonner';
import { toast, Toaster } from 'sonner';
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;
}
interface ContextProps {
doExportData: (sorting: any, filter: any) => Promise<any>;
doExportExcel: () => Promise<void>;
doExportPDF: () => Promise<void>;
}
const initialProps: ContextProps = {
doExportData: async () => ({ data: [], totalCount: 0 })
doExportData: async () => ({ data: [], totalCount: 0 }),
doExportExcel: async () => {},
doExportPDF: async () => {}
};
export const showDetailWalletContext = createContext<ContextProps>(initialProps);
@ -174,6 +187,336 @@ const ShowDetailWalletDialog = () => {
[GetData, filters, selectedWallet]
);
const getAllTransactionData = async () => {
try {
const response = await GetData(
`${apiConfig.service_wallet}/dashboard/balance/list-balance-detail/${selectedWallet?.ID}`,
{
limit: 10000,
page: 1,
with_deleted: false,
order_field: 'amount',
order_direction: 'DESC',
filter: JSON.stringify(filters)
}
);
return response?.data?.list || [];
} catch (error) {
console.error('Error fetching all transaction data', error);
return [];
}
};
const formatTransactionData = (data: any[]) => {
return data.map((item) => ({
'Transaction Code': item.transaction_code || '-',
'MSISDN Reffer': item.msisdn_reff || '-',
'Transaction Type': item.transaction_type?.name || '-',
Type: item.type || '-',
'Pre Amount': item.pre_amount?.toFixed(2) || '0.00',
Amount: item.amount?.toFixed(2) || '0.00',
'Post Amount': item.post_amount?.toFixed(2) || '0.00',
Category: categoryMap[item.category] || '-',
Date: item.date
? new Date(item.date).toLocaleString('id-ID', {
day: '2-digit',
month: 'short',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
})
: '-',
Purpose: item.purpose || '-',
Notes: item.notes || '-'
}));
};
const doExportExcel = async () => {
try {
const allData = await getAllTransactionData();
const formattedData = formatTransactionData(allData);
const wb = XLSX.utils.book_new();
const ws = XLSX.utils.json_to_sheet([]);
const companyHeader = [
['T•PAY'],
['Telin Digital Solution'],
['Av. Pres. Nicolau Lobato, Dili'],
['Tel: (+670) 74147147 / 147'],
['www.t-pay.tl'],
[''],
['Wallet Statement Details'],
[`Export Date: ${format(new Date(), 'dd MMM yyyy HH:mm:ss')}`],
[`Wallet ID: ${selectedWallet?.ID || '-'}`],
['']
];
const columnHeaders = [
[
'Transaction Code',
'MSISDN Reffer',
'Transaction Type',
'Type',
'Pre Amount',
'Amount',
'Post Amount',
'Category',
'Date',
'Purpose',
'Notes'
]
];
const excelData = [
...companyHeader,
...columnHeaders,
...formattedData.map((item) => Object.values(item))
];
XLSX.utils.sheet_add_aoa(ws, excelData);
const colWidths = [
{ wch: 20 },
{ wch: 15 },
{ wch: 25 },
{ wch: 10 },
{ wch: 12 },
{ wch: 12 },
{ wch: 12 },
{ wch: 15 },
{ wch: 20 },
{ wch: 25 },
{ wch: 25 }
];
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 });
if (ws[titleCell]) {
ws[titleCell].s = {
font: { bold: true, sz: 14 },
alignment: { horizontal: 'center' }
};
}
for (let i = 7; i < 9; i++) {
const cellRef = XLSX.utils.encode_cell({ r: i, c: 0 });
if (ws[cellRef]) {
ws[cellRef].s = {
font: { sz: 11 },
alignment: { horizontal: 'left' }
};
}
}
for (let i = 0; i < 6; 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));
}
for (let col = 0; col < columnHeaders[0].length; col++) {
const cellRef = XLSX.utils.encode_cell({ r: 10, c: col });
if (ws[cellRef]) {
ws[cellRef].s = {
font: { bold: true, color: { rgb: 'FFFFFF' } },
fill: { fgColor: { rgb: '4472C4' } },
alignment: { horizontal: 'center' },
border: {
top: { style: 'thin', color: { rgb: '000000' } },
bottom: { style: 'thin', color: { rgb: '000000' } },
left: { style: 'thin', color: { rgb: '000000' } },
right: { style: 'thin', color: { rgb: '000000' } }
}
};
}
}
for (let row = 11; row < 11 + 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 = {
border: {
top: { style: 'thin', color: { rgb: 'D9D9D9' } },
bottom: { style: 'thin', color: { rgb: 'D9D9D9' } },
left: { style: 'thin', color: { rgb: 'D9D9D9' } },
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');
const fileName = `wallet_statement_${selectedWallet?.ID}_${format(new Date(), 'yyyyMMdd_HHmmss')}.xlsx`;
XLSX.writeFile(wb, fileName);
// console.log('Excel export completed successfully');
} catch (error) {
console.error('Error exporting to Excel:', error);
}
};
// 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);
// Buat instance jsPDF
const pdf = new jsPDF({
orientation: 'landscape', // Ubah ke landscape untuk tabel yang lebar
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' });
// Garis pemisah
pdf.setDrawColor(0, 0, 0);
pdf.setLineWidth(0.5);
pdf.line(20, 52, pdf.internal.pageSize.getWidth() - 20, 52);
// Judul laporan
pdf.setFontSize(16);
pdf.setFont('helvetica', 'bold');
pdf.text('Wallet Statement Details', pdf.internal.pageSize.getWidth() / 2, 62, { align: 'center' });
// 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);
// Siapkan data tabel
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']
]);
// 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);
}
}
});
// 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) {
console.error('Error exporting to PDF:', error);
toast.error('Failed to export PDF');
}
};
const doExportData = async (sorting: any, filter: any) => {
const user = localStorage.getItem(
`${import.meta.env.VITE_APP_NAME}-auth-v${import.meta.env.VITE_APP_VERSION}`
@ -219,7 +562,7 @@ const ShowDetailWalletDialog = () => {
};
return (
<showDetailWalletContext.Provider value={{ doExportData }}>
<showDetailWalletContext.Provider value={{ doExportData, doExportExcel, doExportPDF }}>
<Toaster expand visibleToasts={9} duration={3000} />
<Dialog open={showDetailDialog} onOpenChange={setShowDetailDialog}>
<DialogContent className="w-screen max-w-screen max-h-screen p-4 overflow-hidden">