diff --git a/public/media/file-templates/upload-batch-template.xlsx b/public/media/file-templates/upload-batch-template.xlsx index 2af339b..e31ef5a 100644 Binary files a/public/media/file-templates/upload-batch-template.xlsx and b/public/media/file-templates/upload-batch-template.xlsx differ diff --git a/src/pages/dashboards/home/blocks/ShowDetailWalletDialog.tsx b/src/pages/dashboards/home/blocks/ShowDetailWalletDialog.tsx index 2c64a5f..654817c 100644 --- a/src/pages/dashboards/home/blocks/ShowDetailWalletDialog.tsx +++ b/src/pages/dashboards/home/blocks/ShowDetailWalletDialog.tsx @@ -3,8 +3,9 @@ import { Dialog, DialogBody, DialogContent, + DialogDescription, DialogHeader, - DialogTitle, + DialogTitle } from '@/components/ui/dialog'; import { useCallApi } from '@/hooks'; import { apiConfig } from '@/config/api.config'; @@ -19,24 +20,6 @@ import { } from '@/components/ui/select'; import { DefaultTooltip, KeenIcon } from '@/components'; -interface WalletTransaction { - ID: string; - transaction_code: string; - transaction_type: { - id: string; - name: string; - }; - type: string; - amount: number; - pre_amount: number; - post_amount: number; - category: string; - notes: string; - date: string; - msisdn_reff?: string; - purpose?: string; -} - interface ShowDetailWalletDialogProps { open: boolean; onClose: () => void; @@ -58,107 +41,23 @@ const getDefaultDateRange = () => { }; }; -const columns = [ - { - accessorKey: 'transaction_code', - header: ({ column }: any) => , - enableSorting: false, - meta: { headerClassName: 'min-w-[120px]' }, - }, - { - accessorKey: 'msisdn_reff', - header: ({ column }: any) => , - enableSorting: false, - meta: { headerClassName: 'min-w-[120px]' }, - }, - { - accessorKey: 'transaction_type.name', - header: ({ column }: any) => , - enableSorting: false, - meta: { headerClassName: 'min-w-[180px]' }, - }, - { - accessorKey: 'type', - header: ({ column }: any) => , - enableSorting: false, - meta: { headerClassName: 'min-w-[80px]' }, - }, - { - accessorKey: 'amount', - header: ({ column }: any) => , - enableSorting: false, - cell: (info: any) => info.getValue()?.toFixed(2), - meta: { headerClassName: 'min-w-[100px]' }, - }, - { - accessorKey: 'pre_amount', - header: ({ column }: any) => , - enableSorting: false, - cell: (info: any) => info.getValue()?.toFixed(2), - meta: { headerClassName: 'min-w-[100px]' }, - }, - { - accessorKey: 'post_amount', - header: ({ column }: any) => , - enableSorting: false, - cell: (info: any) => info.getValue()?.toFixed(2), - meta: { headerClassName: 'min-w-[100px]' }, - }, - { - accessorKey: 'category', - header: ({ column }: any) => , - enableSorting: false, - meta: { headerClassName: 'min-w-[80px]' }, - }, - { - accessorKey: 'date', - header: ({ column }: any) => , - enableSorting: false, - cell: (info: any) => { - const val = info.getValue(); - if (!val) return '-'; - return new Date(val).toLocaleString('id-ID', { - day: '2-digit', - month: 'short', - year: 'numeric', - hour: '2-digit', - minute: '2-digit', - }); - }, - meta: { headerClassName: 'min-w-[150px]' }, - }, - { - accessorKey: 'purpose', - header: ({ column }: any) => , - enableSorting: false, - cell: (info: any) => info.getValue() || '-', - meta: { headerClassName: 'min-w-[200px]' }, - }, - { - accessorKey: 'notes', - header: ({ column }: any) => , - enableSorting: false, - cell: (info: any) => info.getValue() || '-', - meta: { headerClassName: 'min-w-[200px]' }, - }, -]; - const ShowDetailWalletDialog: React.FC = ({ open, onClose, idbalance, }) => { const { GetData } = useCallApi(); - const [data, setData] = useState([]); - const [isLoading, setIsLoading] = useState(false); + const [dateRange, setDateRange] = useState(getDefaultDateRange()); const [selectedTransferType, setSelectedTransferType] = useState(null); + const [transferType, setTransferType] = useState([]); const [selectedCategory, setSelectedCategory] = useState(null); const [searchValue, setSearchValue] = useState(''); - const [transferType, setTransferType] = useState([]); const [category, setCategory] = useState([]); + const [transaction, setTransaction] = useState([]); + const [filters, setFilters] = useState({}); - const fetchTransferType = async () => { + const fetchTransferType = useCallback(async () => { try { const response = await GetData(`${apiConfig.service_transaction}/transactiontype/list`, { limit: 100, @@ -171,71 +70,18 @@ const ShowDetailWalletDialog: React.FC = ({ } catch (error) { console.error('Error fetching transfer types', error); } - }; + }, [GetData]); - const getDetailList = useCallback( - async ( - limit: number, - page: number, - with_deleted: boolean, - order_field: string, - order_direction: string, - filter: any - ): Promise => { - if (!idbalance) return []; - try { - const response = await GetData( - `${apiConfig.service_wallet}/dashboard/balance/list-balance-detail/${idbalance}`, - { - limit, - page, - with_deleted, - order_field, - order_direction, - filter: JSON.stringify(filter), - } - ); - return response?.data?.list || []; - } catch (error) { - console.error('Error fetching wallet detail:', error); - return []; - } - }, - [GetData, idbalance] - ); + useEffect(() => { + fetchTransferType(); + }, [fetchTransferType]); - const fetchData = useCallback( - async (params: { - pageIndex: number; - pageSize: number; - sorting: Array<{ id: string; desc: boolean }>; - columnFilters: any[]; - }): Promise<{ data: WalletTransaction[] }> => { - // setIsLoading(true); - const order_field = params.sorting.length ? params.sorting[0].id : 'date'; - const order_direction = params.sorting.length && params.sorting[0].desc ? 'DESC' : 'ASC'; - const result = await getDetailList( - params.pageSize, - params.pageIndex + 1, - false, - order_field, - order_direction, - params.columnFilters - ); - setData(result); - setIsLoading(false); - return { data: result }; - }, - [getDetailList] - ); - - // Ambil unique category dari data transaksi setelah data di-fetch useEffect(() => { const uniqueCategories = Array.from( - new Set(data.map((tx) => tx.category).filter((cat) => !!cat)) + new Set(transaction.map((tx) => tx.category).filter((cat) => !!cat)) ); setCategory(uniqueCategories); - }, [data]); + }, [transaction]); const handleTransferTypeChange = (value: string) => { setSelectedTransferType(value === 'all' ? null : value); @@ -250,30 +96,151 @@ const ShowDetailWalletDialog: React.FC = ({ setSelectedTransferType(null); setSelectedCategory(null); setSearchValue(''); + setFilters({}); }; - useEffect(() => { - fetchTransferType(); - }, []); + const handleApplyFilters = () => { + const appliedFilters: any = {}; - useEffect(() => { - if (!open) return; + if (dateRange.from && dateRange.to) { + const fromDate = new Date(`${dateRange.from}T00:00:00Z`); + const toDate = new Date(`${dateRange.to}T23:59:59Z`); + appliedFilters.date = { + from: fromDate.toISOString(), + to: toDate.toISOString(), + }; + } - const filters = []; + if (selectedTransferType) { + appliedFilters.transaction_type_id = selectedTransferType; + } - if (dateRange.from) filters.push({ id: 'date', value: { gte: dateRange.from } }); - if (dateRange.to) filters.push({ id: 'date', value: { lte: dateRange.to } }); - if (selectedTransferType) filters.push({ id: 'transaction_type.id', value: selectedTransferType }); - if (selectedCategory) filters.push({ id: 'category', value: selectedCategory }); - if (searchValue) filters.push({ id: 'transaction_code', value: searchValue }); + if (selectedCategory) { + appliedFilters.category = selectedCategory; + } - fetchData({ - pageIndex: 0, - pageSize: 10, - sorting: [{ id: 'date', desc: true }], - columnFilters: filters, - }); - }, [dateRange, selectedTransferType, selectedCategory, searchValue, open]); + if (searchValue) { + appliedFilters.transaction_code = searchValue; + } + + setFilters(appliedFilters); + }; + + + const columns = [ + { + accessorKey: 'transaction_code', + header: ({ column }: any) => , + enableSorting: false, + meta: { headerClassName: 'min-w-[120px]' }, + }, + { + accessorKey: 'msisdn_reff', + header: ({ column }: any) => , + enableSorting: false, + meta: { headerClassName: 'min-w-[120px]' }, + }, + { + accessorKey: 'transaction_type.name', + header: ({ column }: any) => , + enableSorting: false, + meta: { headerClassName: 'min-w-[180px]' }, + }, + { + accessorKey: 'type', + header: ({ column }: any) => , + enableSorting: false, + meta: { headerClassName: 'min-w-[80px]' }, + }, + { + accessorKey: 'amount', + header: ({ column }: any) => , + enableSorting: false, + cell: (info: any) => info.getValue()?.toFixed(2), + meta: { headerClassName: 'min-w-[100px]' }, + }, + { + accessorKey: 'pre_amount', + header: ({ column }: any) => , + enableSorting: false, + cell: (info: any) => info.getValue()?.toFixed(2), + meta: { headerClassName: 'min-w-[100px]' }, + }, + { + accessorKey: 'post_amount', + header: ({ column }: any) => , + enableSorting: false, + cell: (info: any) => info.getValue()?.toFixed(2), + meta: { headerClassName: 'min-w-[100px]' }, + }, + { + accessorKey: 'category', + header: ({ column }: any) => , + enableSorting: false, + meta: { headerClassName: 'min-w-[80px]' }, + }, + { + accessorKey: 'date', + header: ({ column }: any) => , + enableSorting: false, + cell: (info: any) => { + const val = info.getValue(); + if (!val) return '-'; + return new Date(val).toLocaleString('id-ID', { + day: '2-digit', + month: 'short', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); + }, + meta: { headerClassName: 'min-w-[150px]' }, + }, + { + accessorKey: 'purpose', + header: ({ column }: any) => , + enableSorting: false, + cell: (info: any) => info.getValue() || '-', + meta: { headerClassName: 'min-w-[200px]' }, + }, + { + accessorKey: 'notes', + header: ({ column }: any) => , + enableSorting: false, + cell: (info: any) => info.getValue() || '-', + meta: { headerClassName: 'min-w-[200px]' }, + }, + ]; + + const getTransactionLists = useCallback( + async (page: number, limit: number, sorting: any, _columnFilters: any) => { + // console.log(idbalance); + try { + const response = await GetData(`${apiConfig.service_wallet}/dashboard/balance/list-balance-detail/${idbalance}`, { + limit, + page: page + 1, + with_deleted: false, + order_field: "created_at", + order_direction: 'DESC', + filter: JSON.stringify(filters) + }); + + // console.log(filters); + + if (!response || !response.data) { + console.warn('No data received:', response); + return { data: [], totalCount: 0 }; + } + + setTransaction(response.data.list); + return { data: response.data.list, totalCount: response.data.total_count }; + } catch (error) { + console.error('Error fetching transaction', error); + return { data: [], totalCount: 0 }; + } + }, + [GetData, idbalance, filters] + ); return ( @@ -281,98 +248,98 @@ const ShowDetailWalletDialog: React.FC = ({ Details Wallet Statement + + -
- {/*
-
- +
+
+ - + -
- -
- -
- -
- - - - - - +
+
-
*/} +
+ +
+ + + + + + + + +
+
+ +
{ - return await fetchData({ - pageIndex: params.pageIndex, - pageSize: params.pageSize, - sorting: params.sorting ?? [], - columnFilters: params.columnFilters ?? [], - }); - }} + sorting={[{ id: 'id', desc: false }]} + serverSide={true} + onFetchData={({ pageIndex, pageSize, sorting, columnFilters }) => + getTransactionLists(pageIndex, pageSize, sorting, columnFilters) + } />
@@ -381,4 +348,4 @@ const ShowDetailWalletDialog: React.FC = ({ ); }; -export default ShowDetailWalletDialog; \ No newline at end of file +export default ShowDetailWalletDialog; diff --git a/src/pages/disbursement/history-transaction/HistoryTransaction.tsx b/src/pages/disbursement/history-transaction/HistoryTransaction.tsx index 61b9d2d..792a69f 100644 --- a/src/pages/disbursement/history-transaction/HistoryTransaction.tsx +++ b/src/pages/disbursement/history-transaction/HistoryTransaction.tsx @@ -23,7 +23,7 @@ const HistoryTransactionDisbursement = () => { - History Disbursement + Disbursement
diff --git a/src/pages/disbursement/history-transaction/blocks/DetailTransaction.tsx b/src/pages/disbursement/history-transaction/blocks/DetailTransaction.tsx index b288d0a..81eeec5 100644 --- a/src/pages/disbursement/history-transaction/blocks/DetailTransaction.tsx +++ b/src/pages/disbursement/history-transaction/blocks/DetailTransaction.tsx @@ -94,66 +94,65 @@ const DetailTransaction = () => { } }, [showDetailDialog]); -const handleExport = async () => { - if (!selectedTransactionId || !transactionDetails) return; + const handleExport = async () => { + if (!selectedTransactionId || !transactionDetails) return; - setIsExporting(true); - try { - const response = await fetch( - `${API_URL}/transaction/export?id_disbursment=${selectedTransactionId}`, - { - method: 'GET', - headers: { - Authorization: `Bearer ${getAuth()?.access_token}` + setIsExporting(true); + try { + const response = await fetch( + `${API_URL}/transaction/export?id_disbursment=${selectedTransactionId}`, + { + method: 'GET', + headers: { + Authorization: `Bearer ${getAuth()?.access_token}` + } + } + ); + + if (!response.ok) { + throw new Error('Failed to fetch file'); + } + + const blob = await response.blob(); + const contentDisposition = response.headers.get('content-disposition'); + + const executionDate = transactionDetails.execution_date; + const formattedDate = executionDate + ? moment(executionDate).format('YYYYMMDD_HHmm') + : moment().format('YYYYMMDD_HHmm'); + + let filename = `transaction_${formattedDate}.xlsx`; + + if (contentDisposition) { + const filenameMatch = contentDisposition.match(/filename\*?=(?:UTF-8'')?"?([^;"\n]*)"?/); + if (filenameMatch && filenameMatch[1]) { + filename = decodeURIComponent(filenameMatch[1]); } } - ); - if (!response.ok) { - throw new Error('Failed to fetch file'); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + a.click(); + window.URL.revokeObjectURL(url); + + toast.success('Export successful'); + } catch (error) { + console.error('Error exporting transaction:', error); + toast.error('Failed to export transaction'); + } finally { + setIsExporting(false); } - - const blob = await response.blob(); - const contentDisposition = response.headers.get('content-disposition'); - - const executionDate = transactionDetails.execution_date; - const formattedDate = executionDate - ? moment(executionDate).format('YYYYMMDD_HHmm') - : moment().format('YYYYMMDD_HHmm'); - - let filename = `transaction_${formattedDate}.xlsx`; - - if (contentDisposition) { - const filenameMatch = contentDisposition.match(/filename\*?=(?:UTF-8'')?"?([^;"\n]*)"?/); - if (filenameMatch && filenameMatch[1]) { - filename = decodeURIComponent(filenameMatch[1]); - } - } - - const url = window.URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = filename; - a.click(); - window.URL.revokeObjectURL(url); - - toast.success('Export successful'); - } catch (error) { - console.error('Error exporting transaction:', error); - toast.error('Failed to export transaction'); - } finally { - setIsExporting(false); - } -}; - + }; return ( - + Transaction Details - +
{/* Summary Info */} {transactionDetails && ( @@ -213,7 +212,6 @@ const handleExport = async () => {
)} - {/* Log Table */}
{isLoading ? (
@@ -229,96 +227,113 @@ const handleExport = async () => {

Loading Logs Details...

) : ( - - - - - - - - - - - - - - - - - {transactionDetails?.log && transactionDetails.log.length > 0 ? ( - transactionDetails.log.map((log: any, index: number) => ( - - - - - - - - - - - + <> +
+ +
+
+
UsernameFullnameAmountStatusProcess DateInvoice NumberRemark 1Remark 2Remark 3Actions
- {log.customer?.username ?? 'Not Found'} - - {log.customer?.fullname ?? 'Not Found'} - {log.amount ?? '-'} - {renderStatusBadge(log.status) ?? '-'} - - {log.request_date && moment(log.request_date).isValid() - ? moment(log.request_date).format('DD/MM/YYYY HH:mm') - : '-'} - - {log.reference ?? '-'} - {log.remark_1 ?? '-'}{log.remark_2 ?? '-'}{log.remark_3 ?? '-'} - -
+ + + + + + + + + + + + - )) - ) : ( - - - - )} - -
UsernameFullnameAmountStatus + Process Date + + Invoice Number + Remark 1Remark 2Remark 3Actions
- No logs available -
+ + + {transactionDetails?.log && transactionDetails.log.length > 0 ? ( + transactionDetails.log.map((log: any, index: number) => ( + + + {log.customer?.username ?? 'Not Found'} + + + {log.customer?.fullname ?? 'Not Found'} + + + {log.amount ?? '-'} + + + {renderStatusBadge(log.status) ?? '-'} + + + {log.request_date && moment(log.request_date).isValid() + ? moment(log.request_date).format('DD/MM/YYYY HH:mm') + : '-'} + + + {log.reference ?? '-'} + + + {log.remark_1 ?? '-'} + + + {log.remark_2 ?? '-'} + + + {log.remark_3 ?? '-'} + + + + + + )) + ) : ( + + + No logs available + + + )} + + +
+ )}
- - {/* Export Button - Moved to bottom right after table */} -
- -
@@ -326,4 +341,4 @@ const handleExport = async () => { ); }; -export default DetailTransaction; \ No newline at end of file +export default DetailTransaction; diff --git a/src/pages/disbursement/history-transaction/hooks/TransactionContext.tsx b/src/pages/disbursement/history-transaction/hooks/TransactionContext.tsx index 50829d6..861dcea 100644 --- a/src/pages/disbursement/history-transaction/hooks/TransactionContext.tsx +++ b/src/pages/disbursement/history-transaction/hooks/TransactionContext.tsx @@ -285,7 +285,7 @@ const TransactionProvider = ({ children }: { children: React.ReactNode }) => { pagination={{ size: 10 }} toolbar={} layout={{ card: true }} - sorting={[{ id: 'execution_date', desc: false }]} + sorting={[{ id: 'execution_date', desc: true }]} serverSide={true} onFetchData={({ pageIndex, pageSize, sorting, columnFilters }) => getTransactionLists(pageIndex, pageSize, sorting, columnFilters) diff --git a/src/pages/transaction/approval-transaction/blocks/ListToolbar.tsx b/src/pages/transaction/approval-transaction/blocks/ListToolbar.tsx index a81aea3..559857f 100644 --- a/src/pages/transaction/approval-transaction/blocks/ListToolbar.tsx +++ b/src/pages/transaction/approval-transaction/blocks/ListToolbar.tsx @@ -14,76 +14,61 @@ import { const ListToolbar = () => { const { table, reload } = useDataGrid(); - // Set the initial state for trxDate + // State untuk filter const [trxDate, settrxDate] = useState({ from: '', to: '' }); - const [statusApproval, setStatusApproval] = useState( - (table.getColumn('status_approve')?.getFilterValue() as string) ?? '' - ); + const [statusApproval, setStatusApproval] = useState(''); + const [searchValue, setSearchValue] = useState(''); + const [typeSearchValue, setSearchTypeValue] = useState(''); - useEffect(() => { - const timer = setTimeout(() => { - table.getColumn('status_approve')?.setFilterValue(statusApproval); - table.setPageIndex(0); - }, 200); - return () => clearTimeout(timer); - }, [statusApproval, table]); - - // Function to format date to YYYY-MM-DD - const formatDate = (date: Date): string => { - return date.toISOString().split('T')[0]; - }; - - const [searchValue, setSearchValue] = useState(); - const [typeSearchValue, setSearchTypeValue] = useState(); - - - - // useEffect to set the default date values + // Set tanggal default saat pertama mount useEffect(() => { const today = new Date(); - // const firstDayOfMonth = new Date(today.getFullYear(), today.getMonth(), 1); + const formatDate = (date: Date) => date.toISOString().split('T')[0]; settrxDate({ from: formatDate(today), to: formatDate(today), }); }, []); + // Fungsi untuk apply semua filter sekaligus saat tombol Filter ditekan const handleFilterData = useCallback(() => { try { + if (!trxDate.from || !trxDate.to) { + toast.error('Please select From and To dates'); + return; + } + + // Set filter tanggal table.getColumn('transaction_date')?.setFilterValue(trxDate); + + // Set filter status approval + table.getColumn('status_approve')?.setFilterValue(statusApproval || ''); + + // Set filter search type sebagai column filter id 'searchtype' + table.setColumnFilters((prevFilters) => { + // Hapus dulu filter dengan id 'searchtype' dan 'code' agar gak duplikat + const filtered = prevFilters.filter( + (f) => f.id !== 'searchtype' && f.id !== 'code' + ); + + // Tambahkan filter baru jika ada + if (typeSearchValue) { + filtered.push({ id: 'searchtype', value: typeSearchValue }); + } + if (searchValue) { + filtered.push({ id: 'code', value: searchValue }); + } + + return filtered; + }); + + // Reset ke halaman pertama + table.setPageIndex(0); } catch (error) { toast.error('Error applying filter'); - console.error('Error applying filter:', error); + console.error(error); } - }, [trxDate, table]); - - useEffect(() => { - const timer = setTimeout(() => { - // Add search type filter to column filters - table.setColumnFilters((prev) => [ - ...prev.filter((f) => f.id !== 'searchtype'), - { id: 'searchtype', value: typeSearchValue }, - ]); - table.setPageIndex(0); - }, 200); - return () => clearTimeout(timer); - }, [typeSearchValue, table]); - - - useEffect(() => { - const timer = setTimeout(() => { - table.getColumn('code')?.setFilterValue(searchValue); - table.setPageIndex(0); - }, 200); - - return () => clearTimeout(timer); - }, [searchValue, table]); - - useEffect(() => { - if (trxDate.from && trxDate.to) { - handleFilterData(); - } - }, [trxDate]); + }, [trxDate, statusApproval, searchValue, typeSearchValue, table]); return (
@@ -93,11 +78,8 @@ const ListToolbar = () => { From - settrxDate({ ...trxDate, from: event.target.value }) - } + onChange={(e) => settrxDate({ ...trxDate, from: e.target.value })} name="from" /> @@ -106,20 +88,15 @@ const ListToolbar = () => { To - settrxDate({ ...trxDate, to: event.target.value }) - } + onChange={(e) => settrxDate({ ...trxDate, to: e.target.value })} name="to" /> { - setSearchTypeValue(value); - }} + onValueChange={setSearchTypeValue} > @@ -151,12 +126,16 @@ const ListToolbar = () => { setSearchValue(event.target.value)} + onChange={(e) => setSearchValue(e.target.value)} /> + {/* Tombol Filter */} +
@@ -166,23 +145,22 @@ const ListToolbar = () => { className="h-7.5" onClick={() => { const today = new Date(); - // const firstDayOfMonth = new Date(today.getFullYear(), today.getMonth(), 1); + const formatDate = (date: Date) => date.toISOString().split('T')[0]; - // Resetting all filters + // Reset semua filter dan tanggal ke hari ini setSearchValue(''); setStatusApproval(''); - settrxDate({ - from: formatDate(today), - to: formatDate(today), - }); - + settrxDate({ from: formatDate(today), to: formatDate(today) }); setSearchTypeValue(''); + // Reset filter di tabel table.getColumn('code')?.setFilterValue(''); table.getColumn('status_approve')?.setFilterValue(''); + table.getColumn('transaction_date')?.setFilterValue(''); + table.setColumnFilters([]); table.setPageIndex(0); - reload(); // Reload table data + reload(); }} > diff --git a/src/pages/transaction/history-transaction/blocks/ListToolbar.tsx b/src/pages/transaction/history-transaction/blocks/ListToolbar.tsx index 2a56ce4..72987dc 100644 --- a/src/pages/transaction/history-transaction/blocks/ListToolbar.tsx +++ b/src/pages/transaction/history-transaction/blocks/ListToolbar.tsx @@ -1,7 +1,7 @@ import { DefaultTooltip, KeenIcon, useDataGrid } from '@/components'; import { useTransactionContext } from '../hooks/useTransactionContext'; import { Button } from '@/components/ui/button'; -import { useCallback, useState, useEffect } from 'react'; +import { useCallback, useState, useEffect, useRef } from 'react'; import { toast } from 'sonner'; import { Select, @@ -17,8 +17,8 @@ import { getAuth } from '@/auth'; const ListToolbar = () => { const { table, reload } = useDataGrid(); const [trxDate, settrxDate] = useState({ from: '', to: '' }); - const [searchValue, setSearchValue] = useState(''); // default: empty string - const [typeSearchValue, setSearchTypeValue] = useState(''); // default: empty string + const [searchValue, setSearchValue] = useState(''); + const [typeSearchValue, setSearchTypeValue] = useState(''); const [typeValue, setTypeValue] = useState( (table.getState().columnFilters.find(f => f.id === 'kind')?.value as string) ?? '' ); @@ -26,62 +26,44 @@ const ListToolbar = () => { const { GetData } = useCallApi(); const API_URL = apiConfig.transaction; - const formatDate = (date: Date): string => { - return date.toISOString().split('T')[0]; - }; + const formatDate = (date: Date): string => date.toISOString().split('T')[0]; + // Set tanggal default hari ini saat mount useEffect(() => { const today = new Date(); - settrxDate({ - from: formatDate(today), - to: formatDate(today), - }); + const formatted = formatDate(today); + settrxDate({ from: formatted, to: formatted }); }, []); - useEffect(() => { - const timer = setTimeout(() => { - table.setColumnFilters((prev) => [ - ...prev.filter((f) => f.id !== 'kind'), - { id: 'kind', value: typeValue }, - ]); - table.setPageIndex(0); - }, 200); - return () => clearTimeout(timer); - }, [typeValue, table]); - - useEffect(() => { - const timer = setTimeout(() => { - table.setColumnFilters((prev) => [ - ...prev.filter((f) => f.id !== 'searchtype'), - { id: 'searchtype', value: typeSearchValue }, - ]); - table.setPageIndex(0); - }, 200); - return () => clearTimeout(timer); - }, [typeSearchValue, table]); - - useEffect(() => { - const timer = setTimeout(() => { - table.getColumn('code')?.setFilterValue(searchValue); - table.setPageIndex(0); - }, 200); - return () => clearTimeout(timer); - }, [searchValue, table]); + // **Hilangkan semua useEffect yang auto apply filter saat input berubah** + // Karena kamu mau filter apply hanya lewat tombol Filter + // Fungsi untuk apply SEMUA filter sekaligus saat tombol Filter diklik const handleFilterData = useCallback(() => { - try { - table.getColumn('transaction_date')?.setFilterValue(trxDate); - } catch (error) { - toast.error('Error applying filter'); - console.error('Error applying filter:', error); + if (!trxDate.from || !trxDate.to) { + toast.error('Please select both From and To dates'); + return; } - }, [trxDate, table]); - useEffect(() => { - if (trxDate.from && trxDate.to) { - handleFilterData(); - } - }, [trxDate]); + // Terapkan filter tanggal + table.getColumn('transaction_date')?.setFilterValue(trxDate); + + // Terapkan filter jenis transaksi (kind) + table.setColumnFilters((prev) => { + // Hapus filter 'kind' dan 'searchtype' agar bisa set ulang + const others = prev.filter(f => f.id !== 'kind' && f.id !== 'searchtype' && f.id !== 'code'); + // Bangun array baru dengan filter yang diinginkan + const filters: any[] = [...others]; + + if (typeValue) filters.push({ id: 'kind', value: typeValue }); + if (typeSearchValue) filters.push({ id: 'searchtype', value: typeSearchValue }); + if (searchValue) filters.push({ id: 'code', value: searchValue }); + + return filters; + }); + + table.setPageIndex(0); + }, [trxDate, typeValue, typeSearchValue, searchValue, table]); const exporDataToExcel = async ( typeSearchValue: string, @@ -93,8 +75,8 @@ const ListToolbar = () => { const formattedFilter: any = { "Transactions.transaction_date": { from: `${trxDate.from} 00:00:00`, - to: `${trxDate.to} 23:59:59` - } + to: `${trxDate.to} 23:59:59`, + }, }; if (typeSearchValue === 'msisdn') { @@ -106,17 +88,20 @@ const ListToolbar = () => { } if (typeValue) { - formattedFilter["Transactions.kind"] = typeValue; + formattedFilter['Transactions.kind'] = typeValue; } const filterParam = encodeURIComponent(JSON.stringify(formattedFilter)); - const response = await fetch(`${API_URL}/transaction/export?filter=${filterParam}`, { - method: 'GET', - headers: { - Authorization: `Bearer ${getAuth()?.access_token}`, // ganti dengan token kamu + const response = await fetch( + `${API_URL}/transaction/export?filter=${filterParam}`, + { + method: 'GET', + headers: { + Authorization: `Bearer ${getAuth()?.access_token}`, + }, } - }); + ); if (!response.ok) { throw new Error('Failed to fetch file'); @@ -161,16 +146,11 @@ const ListToolbar = () => { name="to" /> - - setTypeValue(value)}> - TRANSFER PURCHASE WITHDRAW @@ -193,10 +173,7 @@ const ListToolbar = () => { - setSearchTypeValue(value)}> @@ -216,6 +193,11 @@ const ListToolbar = () => { onChange={(event) => setSearchValue(event.target.value)} /> + + +
@@ -245,28 +227,17 @@ const ListToolbar = () => { Export Data - - - -
-
- -
- {isLoading ? ( -
Loading details...
- ) : filteredTransactions.length > 0 ? ( -
-
- - - - - - - - - - - - - - - - {filteredTransactions.map((transaction) => ( - - - - - - - - - - - - ))} - -
Transaction CodeTransaction TypeTypeAmountPre AmountPost AmountCategoryDateNotes
{transaction.transaction_code} - {transaction.transaction_type?.name || 'N/A'} - {transaction.type}{transaction.amount.toFixed(2)}{transaction.pre_amount.toFixed(2)}{transaction.post_amount.toFixed(2)}{transaction.category} - {new Date(transaction.date).toLocaleString('id-ID', { - day: '2-digit', - month: 'short', - year: 'numeric', - hour: '2-digit', - minute: '2-digit' - })} - {transaction.notes || '-'}
-
-
- ) : ( -
No transaction data available
- )} -
-
- -
- ); -}; - -export default ShowDialog; diff --git a/src/pages/wallet/wallet-history/hooks/useManageWalletHistoryContext.tsx b/src/pages/wallet/wallet-history/hooks/useManageWalletHistoryContext.tsx deleted file mode 100644 index 9e5f912..0000000 --- a/src/pages/wallet/wallet-history/hooks/useManageWalletHistoryContext.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { useContext } from 'react'; -import { ManageWalletContext } from './ManageWalletHistoryContext'; - -const useManageWalletContext = () => { - const context = useContext(ManageWalletContext); - if (!context) { - throw new Error('useManageWalletContext must be used within a ManageWalletContextProvider'); - } - return context; -}; - -export { useManageWalletContext }; diff --git a/src/pages/wallet/wallet-history/WalletHistory.tsx b/src/pages/wallet/wallet-statement/WalletStatement.tsx similarity index 93% rename from src/pages/wallet/wallet-history/WalletHistory.tsx rename to src/pages/wallet/wallet-statement/WalletStatement.tsx index 641ba29..ab6a685 100644 --- a/src/pages/wallet/wallet-history/WalletHistory.tsx +++ b/src/pages/wallet/wallet-statement/WalletStatement.tsx @@ -1,9 +1,9 @@ import { Container, DataGridInner } from '@/components'; -import { ManageWalletContextProvider } from './hooks/ManageWalletHistoryContext'; import { Breadcrumbs, Link } from '@mui/material'; import { Helmet } from 'react-helmet'; +import { ManageWalletContextProvider } from './hooks/ManageWalletStatementContext'; -const WalletHistory = () => { +const WalletStatement = () => { return ( <> @@ -35,4 +35,4 @@ const WalletHistory = () => { ); }; -export default WalletHistory; +export default WalletStatement; diff --git a/src/pages/wallet/wallet-history/blocks/ListToolbar.tsx b/src/pages/wallet/wallet-statement/blocks/ListToolbar.tsx similarity index 100% rename from src/pages/wallet/wallet-history/blocks/ListToolbar.tsx rename to src/pages/wallet/wallet-statement/blocks/ListToolbar.tsx diff --git a/src/pages/wallet/wallet-statement/blocks/ShowDetailDialog.tsx b/src/pages/wallet/wallet-statement/blocks/ShowDetailDialog.tsx new file mode 100644 index 0000000..33d7ede --- /dev/null +++ b/src/pages/wallet/wallet-statement/blocks/ShowDetailDialog.tsx @@ -0,0 +1,503 @@ +import { useEffect, useState } from 'react'; +import { + Dialog, + DialogBody, + DialogContent, + DialogHeader, + DialogTitle +} from '@/components/ui/dialog'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from '@/components/ui/select'; +import { useManageStatementContext } from '../hooks/useManageWalletStatementContext'; +import { DefaultTooltip, KeenIcon } from '@/components'; +import { toast } from 'sonner'; +import { Button } from '@/components/ui/button'; +import { useCallApi } from '@/hooks'; +import { apiConfig } from '@/config/api.config'; + +const API_URL_WALLET = apiConfig.service_wallet; +const API_URL = apiConfig.service_transaction; + +interface TransactionType { + id: string; + name: string; +} + +interface WalletTransaction { + ID: string; + id_balance: string; + transaction_code: string; + transaction_type: { + id: string; + name: string; + }; + type: string; + amount: number; + pre_amount: number; + post_amount: number; + category: string; + notes: string; + date: string; + msisdn_reff: string; + purpose: string; +} + +const ShowDialog = () => { + const { showDetailDialog, setShowDetailDialog, selectedWallet } = useManageStatementContext(); + const [isLoading, setIsLoading] = useState(false); + const { GetData } = useCallApi(); + + const [transactionType, setTransactionType] = useState([]); + const [filteredTransactions, setFilteredTransactions] = useState([]); + + const [currentPage, setCurrentPage] = useState(1); + const [totalItems, setTotalItems] = useState(0); + const [totalPages, setTotalPages] = useState(1); + const itemsPerPage = 10; + + const getDefaultDateRange = () => { + const today = new Date(); + today.setHours(23, 59, 59, 999); + + const sevenDaysAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000); + sevenDaysAgo.setHours(0, 0, 0, 0); + + return { + from: formatDate(sevenDaysAgo, true), + to: formatDate(today, true) + }; + }; + + const formatDate = (date: Date, includeTime = false) => { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + + if (includeTime) { + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`; + } + + return `${year}-${month}-${day}`; + }; + + const [dateRange, setDateRange] = useState(getDefaultDateRange()); + const [category, setCategory] = useState(null); + const [selectedCategory, setSelectedCategory] = useState(''); + const [selectedTransactionType, setSelectedTransactionType] = useState(''); + const [searchValue, setSearchValue] = useState(''); + + const TransactionKind = { + return: 'R', + transfer: 'T', + purchase: 'P', + purchase_loja: 'L', + withdraw: 'W', + topup: 'U', + topup_p24: 'B', + topup_partner: 'N', + reward: 'E', + transfer_agent: 'A', + withdraw_agent: 'M', + topup_agent: 'O', + transfer_p24: 'S', + withdraw_merchant: 'I', + donation: 'D', + fee: 'F', + raversal: 'V', + cashback_cash: 'C', + cashback_point: 'H', + withdraw_admin: 'J' + }; + + type TransactionKindValue = (typeof TransactionKind)[keyof typeof TransactionKind]; + + const fetchTransactionType = async () => { + try { + setIsLoading(true); + const response = await GetData(`${API_URL}/transactiontype/list`, { + limit: 100, + page: 1, + with_deleted: false, + order_field: 'created_at', + order_direction: 'ASC' + }); + setTransactionType( + (response?.data?.list || []).sort((a: any, b: any) => a.name.localeCompare(b.name)) + ); + } catch (error) { + console.error('Error fetching Transaction type', error); + toast.error('Failed to load transaction types'); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchTransactionType(); + }, []); + + const fetchFilteredTransactions = async () => { + if (!selectedWallet?.ID) return; + + setIsLoading(true); + try { + let fromDateTime = dateRange.from; + if (!fromDateTime.includes('T')) { + const fromDate = new Date(fromDateTime); + fromDateTime = formatDate(fromDate, true); + } + + let toDateTime = dateRange.to; + if (!toDateTime.includes('T')) { + const toDate = new Date(toDateTime); + toDate.setHours(23, 59, 59, 999); + toDateTime = formatDate(toDate, true); + } + + const response = await GetData( + `${API_URL_WALLET}/dashboard/balance/list-balance-detail/${selectedWallet.ID}`, + { + limit: itemsPerPage, + page: currentPage, + order_field: 'created_at', + order_direction: 'DESC', + start_date: fromDateTime, + end_date: toDateTime, + transaction_type: selectedTransactionType || undefined, + category: selectedCategory || undefined, + search: searchValue || undefined + } + ); + console.log(response); + setFilteredTransactions(response?.data?.list || []); + setTotalItems(response?.data?.total || 0); + setTotalPages(Math.ceil((response?.data?.total || 0) / itemsPerPage)); + } catch (error) { + console.error('Error fetching filtered transactions:', error); + toast.error('Failed to load filtered transactions'); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + if (!showDetailDialog || !selectedWallet?.ID) return; + + const timer = setTimeout(() => { + fetchFilteredTransactions(); + }, 300); + + return () => clearTimeout(timer); + }, [ + showDetailDialog, + dateRange.from, + dateRange.to, + selectedTransactionType, + selectedCategory, + searchValue, + selectedWallet?.ID, + currentPage + ]); + + const handleSearchChange = (e: React.ChangeEvent) => { + const value = e.target.value; + setSearchValue(value); + }; + + const handleDateChange = (type: 'from' | 'to', value: string) => { + const date = new Date(value); + + if (type === 'from') { + date.setHours(0, 0, 0, 0); + } else { + date.setHours(23, 59, 59, 999); + } + + const formattedDate = formatDate(date, true); + console.log(`Setting ${type} date to:`, formattedDate); + setDateRange((prev) => ({ ...prev, [type]: formattedDate })); + }; + + const handleClearAllFilters = () => { + const defaultDates = getDefaultDateRange(); + console.log('Resetting filters to default:', defaultDates); + setDateRange(defaultDates); + setSelectedTransactionType(''); + setSearchValue(''); + setSelectedCategory(''); + setCurrentPage(1); + }; + + const handlePageChange = (page: number) => { + setCurrentPage(page); + }; + + useEffect(() => { + if (!showDetailDialog) return; + + const checkNewDay = () => { + const now = new Date(); + const currentDate = formatDate(now); + const fromDate = new Date(dateRange.from); + + if ( + currentDate !== formatDate(new Date(dateRange.to)) && + now.getTime() - fromDate.getTime() > 7 * 24 * 60 * 60 * 1000 + ) { + setDateRange(getDefaultDateRange()); + toast.info('Date range has been updated to the current period'); + } + }; + + checkNewDay(); + + const interval = setInterval(checkNewDay, 60 * 60 * 1000); + return () => clearInterval(interval); + }, [showDetailDialog, dateRange]); + + const getDisplayDate = (dateTimeString: string) => { + if (!dateTimeString) return ''; + return dateTimeString.split('T')[0]; + }; + + return ( + + + + Details Wallet Statement + + +
+
+ + + + +
+ +
+ +
+ +
+ + + + + + +
+ + {isLoading ? ( +
Loading details...
+ ) : filteredTransactions.length > 0 ? ( +
+
+ + + + + + + + + + + + + + + + + + {filteredTransactions.map((transaction) => ( + + + + + + + + + + + + + + ))} + +
+ Transaction Code + + MSISDN Reffer + + Transaction Type + TypeAmount + Pre Amount + + Post Amount + + Status Kind + + Date Time + Notes + Purpose +
+ {transaction.transaction_code} + + {transaction.msisdn_reff} + + {transaction.transaction_type?.name || 'N/A'} + {transaction.type} + {transaction.amount.toFixed(2)} + + {transaction.pre_amount.toFixed(2)} + + {transaction.post_amount.toFixed(2)} + + {transaction.category} + + {new Date(transaction.date).toLocaleString('id-ID', { + day: '2-digit', + month: 'short', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + })} + + {transaction.notes || '-'} + + {transaction.purpose || '-'} +
+
+
+ ) : ( +
No transaction data available
+ )} +
+
+ Showing {(currentPage - 1) * itemsPerPage + 1} to{' '} + {Math.min(currentPage * itemsPerPage, totalItems)} of {totalItems} entries +
+
+ + +
+ {Array.from({ length: Math.min(5, totalPages) }, (_, i) => { + let pageToShow; + if (totalPages <= 5) { + pageToShow = i + 1; + } else if (currentPage <= 3) { + pageToShow = i + 1; + } else if (currentPage >= totalPages - 2) { + pageToShow = totalPages - 4 + i; + } else { + pageToShow = currentPage - 2 + i; + } + + return ( + + ); + })} +
+ + +
+
+
+
+
+
+ ); +}; + +export default ShowDialog; diff --git a/src/pages/wallet/wallet-history/hooks/ManageWalletHistoryContext.tsx b/src/pages/wallet/wallet-statement/hooks/ManageWalletStatementContext.tsx similarity index 97% rename from src/pages/wallet/wallet-history/hooks/ManageWalletHistoryContext.tsx rename to src/pages/wallet/wallet-statement/hooks/ManageWalletStatementContext.tsx index 629762a..ac3cc27 100644 --- a/src/pages/wallet/wallet-history/hooks/ManageWalletHistoryContext.tsx +++ b/src/pages/wallet/wallet-statement/hooks/ManageWalletStatementContext.tsx @@ -250,14 +250,7 @@ const ManageWalletContextProvider = ({ children }: { children: React.ReactNode } if (Array.isArray(filter)) { filter.forEach((f: any) => { if (f.id === 'msisdn' && f.value) { - filterParams.msisdn = { like: `%${f.value.toLowerCase()}%` }; - } - - if (f.id === 'CreatedAt' && f.value?.from && f.value?.to) { - filterParams.created_at = { - from: `${f.value.from} 00:00:00`, - to: `${f.value.to} 23:59:59` - }; + filterParams.msisdn = f.value; } if (f.id === 'id_wallet' && f.value) { diff --git a/src/pages/wallet/wallet-statement/hooks/useManageWalletStatementContext.tsx b/src/pages/wallet/wallet-statement/hooks/useManageWalletStatementContext.tsx new file mode 100644 index 0000000..dd7c6df --- /dev/null +++ b/src/pages/wallet/wallet-statement/hooks/useManageWalletStatementContext.tsx @@ -0,0 +1,13 @@ +import { useContext } from 'react'; +import { ManageWalletContext } from './ManageWalletStatementContext'; + + +const useManageStatementContext = () => { + const context = useContext(ManageWalletContext); + if (!context) { + throw new Error('useManageStatementContext must be used within a ManageStatementContextProvider'); + } + return context; +}; + +export { useManageStatementContext }; diff --git a/src/routing/AppRoutingSetup.tsx b/src/routing/AppRoutingSetup.tsx index f053db3..1da4e34 100644 --- a/src/routing/AppRoutingSetup.tsx +++ b/src/routing/AppRoutingSetup.tsx @@ -41,7 +41,7 @@ import ProviderMaster from '@/pages/master/provider/ProviderMaster'; import ConversionMaster from '@/pages/master/conversion/ConversionMaster'; import RewardMaster from '@/pages/master/reward/RewardMaster'; import WalletRuleMaster from '@/pages/master/walletRule/WalletRuleMaster'; -import WalletHistory from '@/pages/wallet/wallet-history/WalletHistory'; +import WalletStatement from '@/pages/wallet/wallet-statement/WalletStatement'; import WalletMaster from '@/pages/master/wallet/WalletMaster'; import CurrencyMaster from '@/pages/master/currency/CurrencyMaster'; import FeedbackMemberMaster from '@/pages/members/feedback-member/FeedbackMember'; @@ -101,7 +101,7 @@ const AppRoutingSetup = (): ReactElement => { } /> - } /> + } /> } />