From a7e0fab1ffac8ba070c2080fd0681c4812b30a33 Mon Sep 17 00:00:00 2001 From: Raja Oktafrianto Date: Thu, 5 Jun 2025 15:06:50 +0700 Subject: [PATCH] update --- package.json | 5 + src/pages/notification/blocks/AddDialog.tsx | 77 +++-- src/pages/notification/blocks/useDebounce.tsx | 17 + .../wallet-statement/blocks/FilterSection.tsx | 85 +++-- .../blocks/ShowDetailDialog.tsx | 302 +++++++++++++++++- 5 files changed, 420 insertions(+), 66 deletions(-) create mode 100644 src/pages/notification/blocks/useDebounce.tsx diff --git a/package.json b/package.json index bc73844..be06bb0 100644 --- a/package.json +++ b/package.json @@ -50,10 +50,13 @@ "clsx": "^2.1.1", "cmdk": "^1.0.4", "date-fns": "^3.0.0", + "file-saver": "^2.0.5", "formik": "^2.4.6", "helmet": "^8.1.0", "html2canvas": "^1.4.1", "https": "^1.0.0", + "jspdf": "^3.0.1", + "jspdf-autotable": "^5.0.2", "leaflet": "^1.9.4", "lucide-react": "^0.456.0", "metronic-tailwind-react": "file:", @@ -86,10 +89,12 @@ "tailwind-merge": "^2.5.4", "tailwindcss-animate": "^1.0.7", "vite-plugin-windicss": "^1.9.3", + "xlsx": "^0.18.5", "yup": "^1.4.0" }, "devDependencies": { "@eslint/js": "^9.14.0", + "@types/file-saver": "^2.0.7", "@types/leaflet": "^1.9.14", "@types/node": "^22.9.0", "@types/react": "^18.3.12", diff --git a/src/pages/notification/blocks/AddDialog.tsx b/src/pages/notification/blocks/AddDialog.tsx index 9036f2d..3477d9b 100644 --- a/src/pages/notification/blocks/AddDialog.tsx +++ b/src/pages/notification/blocks/AddDialog.tsx @@ -28,6 +28,7 @@ import { doSaveLogActivity } from '@/actions/GlobalActions'; import { CustomerProps } from '@/pages/master/provider/blocks/AddDialog'; import { Check, ChevronDown } from 'lucide-react'; import { initialStateNotification, selectedNotification, validateFormNotification } from './Types'; +import { useDebounce } from './useDebounce'; const API_URL_CUSTOMER = apiConfig.service_customer; const API_URL_NOTIFICATION = apiConfig.service_notification; @@ -39,6 +40,8 @@ const AddDialog = () => { const [alert, setAlert] = useState({ show: false, message: '' }); const isSubmittingRef = useRef(false); const [errors, setErrors] = useState>({}); + const [searchQuery, setSearchQuery] = useState(''); + const debouncedSearchQuery = useDebounce(searchQuery, 500); const [formField, setFormField] = useState(initialStateNotification); const [open, setOpen] = useState(false); @@ -95,7 +98,6 @@ const AddDialog = () => { } } catch (err) { toast.error('Something went wrong'); - console.log(err); } finally { isSubmittingRef.current = false; setIsSubmitting(false); @@ -104,22 +106,35 @@ const AddDialog = () => { [PostData, formField, handleAddDialog, reload] ); - const getCustomerList = async (sorting: any) => { - try { - sorting = sorting.length == 0 ? [{ id: 'name', desc: false }] : sorting; - const response = await GetData(`${API_URL_CUSTOMER}/customer/list`, { - limit: 100, - page: 1, - with_deleted: false, - order_field: sorting[0].id, - order_direction: sorting[0].desc == false ? 'ASC' : 'DESC' - }); + useEffect(() => { + const fetchCustomers = async () => { + try { + const filters = []; - setCustomers(response?.data.list); - } catch (error) { - console.error('Error fetching customer', error); - } - }; + if (debouncedSearchQuery) { + filters.push({ + id: 'fullname', + value: debouncedSearchQuery + }); + } + + const response = await GetData(`${API_URL_CUSTOMER}/customer/list`, { + limit: 100, + page: 1, + with_deleted: false, + order_field: 'fullname', + order_direction: 'ASC', + filter: JSON.stringify(filters), + specialFilter: filters.length > 0 + }); + setCustomers(response?.data.list); + } catch (error) { + console.error('Error fetching customers', error); + } + }; + + fetchCustomers(); + }, [debouncedSearchQuery]); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); @@ -127,10 +142,6 @@ const AddDialog = () => { doCreateNotification(e); }; - useEffect(() => { - getCustomerList([{ id: 'id', desc: false }]); - }, []); - useEffect(() => { if (showAddDialog === false) { resetForm(); @@ -217,7 +228,11 @@ const AddDialog = () => { - + { return ( { - const customerId = customer.id; - setFormField((prev) => { - const isSelected = prev.customers.includes(customerId); - return { - ...prev, - customers: isSelected - ? prev.customers.filter((id) => id !== customerId) - : [...prev.customers, customerId] - }; - }); + setFormField((prev) => ({ + ...prev, + customers: isSelected + ? prev.customers.filter((id) => id !== customer.id) + : [...prev.customers, customer.id] + })); }} className={isSelected ? 'bg-accent font-semibold' : ''} > {isSelected && } - {customer.username} + {customer.fullname} ({customer.msisdn}) ); diff --git a/src/pages/notification/blocks/useDebounce.tsx b/src/pages/notification/blocks/useDebounce.tsx new file mode 100644 index 0000000..2605ca6 --- /dev/null +++ b/src/pages/notification/blocks/useDebounce.tsx @@ -0,0 +1,17 @@ +import { useState, useEffect } from 'react'; + +export function useDebounce(value: T, delay: number): T { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => { + clearTimeout(handler); + }; + }, [value, delay]); + + return debouncedValue; +} \ No newline at end of file diff --git a/src/pages/wallet/wallet-statement/blocks/FilterSection.tsx b/src/pages/wallet/wallet-statement/blocks/FilterSection.tsx index e2b4084..d4224e1 100644 --- a/src/pages/wallet/wallet-statement/blocks/FilterSection.tsx +++ b/src/pages/wallet/wallet-statement/blocks/FilterSection.tsx @@ -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 = ({ const [selectedCategory, setSelectedCategory] = useState(null); const [category, setCategory] = useState([]); 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 = ({ ); 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 = ({ }; } - if (selectedTransferType) { + if (selectedTransferType) { appliedFilters.id_transaction_type = selectedTransferType; } @@ -200,20 +221,22 @@ const FilterSection: React.FC = ({ Filter Data - - - +
+ + + - - - + + + +
); }; -export default FilterSection; \ No newline at end of file +export default FilterSection; diff --git a/src/pages/wallet/wallet-statement/blocks/ShowDetailDialog.tsx b/src/pages/wallet/wallet-statement/blocks/ShowDetailDialog.tsx index de37075..53e9047 100644 --- a/src/pages/wallet/wallet-statement/blocks/ShowDetailDialog.tsx +++ b/src/pages/wallet/wallet-statement/blocks/ShowDetailDialog.tsx @@ -14,13 +14,20 @@ import { useManageStatementContext } from '../hooks/useManageWalletStatementCont import { 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 ContextProps { doExportData: (sorting: any, filter: any) => Promise; + doExportExcel: () => Promise; + doExportPDF: () => Promise; } const initialProps: ContextProps = { - doExportData: async () => ({ data: [], totalCount: 0 }) + doExportData: async () => ({ data: [], totalCount: 0 }), + doExportExcel: async () => {}, + doExportPDF: async () => {} }; export const showDetailWalletContext = createContext(initialProps); @@ -174,6 +181,297 @@ 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); + } + }; + + const doExportPDF = async () => { + try { + const allData = await getAllTransactionData(); + const formattedData = formatTransactionData(allData); + + const pdf = new jsPDF('l', 'mm', 'a4'); + + pdf.setFontSize(20); + pdf.setFont('helvetica', 'bold'); + pdf.text('T•PAY', 148, 20, { align: 'center' }); + + pdf.setFontSize(12); + pdf.setFont('helvetica', 'normal'); + pdf.text('Telin Digital Solution', 148, 30, { align: 'center' }); + pdf.text('Av. Pres. Nicolau Lobato, Dili', 148, 36, { align: 'center' }); + pdf.text('Tel : (+670) 7414747 / 147', 148, 42, { align: 'center' }); + pdf.text('www.t-pay.tl', 148, 48, { align: 'center' }); + + pdf.setFontSize(14); + pdf.setFont('helvetica', 'bold'); + pdf.text('Wallet Statement Details', 148, 60, { align: 'center' }); + + pdf.setFontSize(10); + pdf.setFont('helvetica', 'normal'); + pdf.text(`Export Date: ${format(new Date(), 'dd MMM yyyy HH:mm:ss')}`, 20, 70); + pdf.text(`Wallet ID: ${selectedWallet?.ID || '-'}`, 20, 76); + + 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'] + ]); + + (pdf as any).autoTable({ + head: [ + [ + 'Transaction Code', + 'MSISDN Reffer', + 'Transaction Type', + 'Type', + 'Pre Amount', + 'Amount', + 'Post Amount', + 'Category', + 'Date', + 'Purpose', + 'Notes' + ] + ], + body: tableData, + startY: 85, + styles: { + fontSize: 8, + cellPadding: 2 + }, + headStyles: { + fillColor: [66, 66, 66], + textColor: [255, 255, 255], + fontStyle: 'bold' + }, + columnStyles: { + 0: { cellWidth: 20 }, + 1: { cellWidth: 20 }, + 2: { cellWidth: 25 }, + 3: { cellWidth: 15 }, + 4: { cellWidth: 18 }, + 5: { cellWidth: 18 }, + 6: { cellWidth: 18 }, + 7: { cellWidth: 25 }, + 8: { cellWidth: 25 }, + 9: { cellWidth: 30 }, + 10: { cellWidth: 30 } + }, + margin: { top: 85, left: 10, right: 10 } + }); + + const fileName = `wallet_statement_${selectedWallet?.ID}_${format(new Date(), 'yyyyMMdd_HHmmss')}.pdf`; + pdf.save(fileName); + + // console.log('PDF export completed successfully'); + } catch (error) { + console.error('Error exporting to PDF:', error); + } + }; + 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 +517,7 @@ const ShowDetailWalletDialog = () => { }; return ( - +