add detail statement on wallet statement

This commit is contained in:
Raja Oktafrianto
2025-05-21 07:36:05 +07:00
parent 4ae995d80c
commit 8e726d2966
4 changed files with 433 additions and 63 deletions

View File

@ -7,11 +7,11 @@ const WalletHistory = () => {
return ( return (
<> <>
<Helmet> <Helmet>
<title>TPAY | Wallet History</title> <title>TPAY | Wallet Statement</title>
</Helmet> </Helmet>
<ManageWalletContextProvider> <ManageWalletContextProvider>
<Container> <Container>
<h1 className="text-xl font-medium leading-none text-gray-900 mb-5">Wallet History</h1> <h1 className="text-xl font-medium leading-none text-gray-900 mb-5">Wallet Statement</h1>
<Breadcrumbs sx={{ mb: 2 }}> <Breadcrumbs sx={{ mb: 2 }}>
<Link underline="none" color="inherit" href="/"> <Link underline="none" color="inherit" href="/">
<span className="text-sm hover:underline">Dashboard</span> <span className="text-sm hover:underline">Dashboard</span>
@ -22,7 +22,7 @@ const WalletHistory = () => {
</Link> </Link>
<Link underline="none" color="inherit"> <Link underline="none" color="inherit">
<span className="text-sm">Wallet History</span> <span className="text-sm">Wallet Statement</span>
</Link> </Link>
</Breadcrumbs> </Breadcrumbs>

View File

@ -38,7 +38,6 @@ const ListToolbar = () => {
const [searchValue, setSearchValue] = useState<string>( const [searchValue, setSearchValue] = useState<string>(
(table.getColumn('msisdn')?.getFilterValue() as string) ?? '' (table.getColumn('msisdn')?.getFilterValue() as string) ?? ''
); );
const [dateRange, setDateRange] = useState({ from: '', to: '' });
const [walletId, setWalletId] = useState<string>( const [walletId, setWalletId] = useState<string>(
(table.getColumn('id_wallet')?.getFilterValue() as string) ?? '' (table.getColumn('id_wallet')?.getFilterValue() as string) ?? ''
); );
@ -48,12 +47,6 @@ const ListToolbar = () => {
const [wallets, setWallets] = useState<WalletProps[]>([]); const [wallets, setWallets] = useState<WalletProps[]>([]);
const [groups, setGroups] = useState<GroupProps[]>([]); const [groups, setGroups] = useState<GroupProps[]>([]);
useEffect(() => {
const today = new Date();
const threeMonthsAgo = getOneMonthsAgo();
setDateRange({ from: formatDate(threeMonthsAgo), to: formatDate(today) });
}, []);
useEffect(() => { useEffect(() => {
const timer = setTimeout(() => { const timer = setTimeout(() => {
table.getColumn('msisdn')?.setFilterValue(searchValue); table.getColumn('msisdn')?.setFilterValue(searchValue);
@ -62,15 +55,6 @@ const ListToolbar = () => {
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, [searchValue, table]); }, [searchValue, table]);
const handleFilterByDate = useCallback(() => {
try {
table.getColumn('CreatedAt')?.setFilterValue(dateRange);
} catch (error) {
toast.error('Error applying date filter');
console.error('Error applying date filter:', error);
}
}, [dateRange, table]);
useEffect(() => { useEffect(() => {
table.getColumn('id_wallet')?.setFilterValue(walletId); table.getColumn('id_wallet')?.setFilterValue(walletId);
table.setPageIndex(0); table.setPageIndex(0);
@ -81,12 +65,6 @@ const ListToolbar = () => {
table.setPageIndex(0); table.setPageIndex(0);
}, [groupId, table]); }, [groupId, table]);
useEffect(() => {
if (dateRange.from && dateRange.to) {
handleFilterByDate();
}
}, [dateRange, handleFilterByDate]);
const fetchWallets = async () => { const fetchWallets = async () => {
try { try {
const response = await GetData(`${API_URL_WALLET}/dashboard/wallet/`, { const response = await GetData(`${API_URL_WALLET}/dashboard/wallet/`, {
@ -129,23 +107,22 @@ const ListToolbar = () => {
from: formatDate(oneMonthAgo), from: formatDate(oneMonthAgo),
to: formatDate(today) to: formatDate(today)
}; };
setSearchValue(''); setSearchValue('');
setWalletId(''); setWalletId('');
setGroupId(''); setGroupId('');
setDateRange(resetDateRange);
table.getColumn('msisdn')?.setFilterValue(''); table.getColumn('msisdn')?.setFilterValue('');
table.getColumn('id_wallet')?.setFilterValue(''); table.getColumn('id_wallet')?.setFilterValue('');
table.getColumn('id_group')?.setFilterValue(''); table.getColumn('id_group')?.setFilterValue('');
table.getColumn('CreatedAt')?.setFilterValue(resetDateRange); table.getColumn('CreatedAt')?.setFilterValue(resetDateRange);
setTimeout(() => { setTimeout(() => {
table.setPageIndex(0); table.setPageIndex(0);
reload(); reload();
}, 0); }, 0);
}; };
const handleRefresh = () => { const handleRefresh = () => {
const today = new Date(); const today = new Date();
const threeMonthsAgo = getOneMonthsAgo(); const threeMonthsAgo = getOneMonthsAgo();
@ -157,7 +134,6 @@ const ListToolbar = () => {
setSearchValue(''); setSearchValue('');
setWalletId(''); setWalletId('');
setGroupId(''); setGroupId('');
setDateRange(resetDateRange);
table.setColumnFilters([{ id: 'CreatedAt', value: resetDateRange }]); table.setColumnFilters([{ id: 'CreatedAt', value: resetDateRange }]);
table.setPageIndex(0); table.setPageIndex(0);
@ -169,24 +145,6 @@ const ListToolbar = () => {
<div className="flex flex-wrap gap-2 lg:gap-5 w-full"> <div className="flex flex-wrap gap-2 lg:gap-5 w-full">
<div className="flex justify-between w-full items-center"> <div className="flex justify-between w-full items-center">
<div className="flex flex-wrap items-end gap-3 w-full"> <div className="flex flex-wrap items-end gap-3 w-full">
<label className="input input-sm w-[160px]">
From
<input
type="date"
value={dateRange.from}
onChange={(e) => setDateRange({ ...dateRange, from: e.target.value })}
/>
</label>
<label className="input input-sm w-[160px]">
To
<input
type="date"
value={dateRange.to}
onChange={(e) => setDateRange({ ...dateRange, to: e.target.value })}
/>
</label>
<label className="input input-sm w-[160px]"> <label className="input input-sm w-[160px]">
<KeenIcon icon="magnifier" /> <KeenIcon icon="magnifier" />
<input <input
@ -252,4 +210,4 @@ const ListToolbar = () => {
); );
}; };
export default ListToolbar; export default ListToolbar;

View File

@ -0,0 +1,363 @@
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 { useManageWalletContext } from '../hooks/useManageWalletHistoryContext';
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 formatDate = (date: Date) => date.toLocaleDateString('sv-SE');
const API_URL_WALLET = apiConfig.service_wallet;
const API_URL = apiConfig.service_transaction;
interface TransferType {
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;
}
const ShowDialog = () => {
const { showDetailDialog, setShowDetailDialog, selectedWallet } = useManageWalletContext();
const [isLoading, setIsLoading] = useState(false);
const { GetData } = useCallApi();
const [transferType, setTransferType] = useState<TransferType[]>([]);
const [transactions, setTransactions] = useState<WalletTransaction[]>([]);
const [filteredTransactions, setFilteredTransactions] = useState<WalletTransaction[]>([]);
const [category, setCategory] = useState<string[]>([]);
const getDefaultDateRange = () => {
const today = new Date();
const sevenDaysAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000);
return {
from: formatDate(sevenDaysAgo),
to: formatDate(today)
};
};
useEffect(() => {
const uniqueCategories = Array.from(
new Set(transactions.map((transaction) => transaction.category))
);
setCategory(uniqueCategories);
}, [transactions]);
const [dateRange, setDateRange] = useState(getDefaultDateRange());
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
const [selectedTransferType, setSelectedTransferType] = useState<string | null>(null);
const [searchValue, setSearchValue] = useState('');
const fetchTransferType = async () => {
try {
const response = await GetData(`${API_URL}/transactiontype/list`, {
limit: 100,
page: 1,
with_deleted: false,
order_field: 'created_at',
order_direction: 'ASC'
});
setTransferType(response?.data?.list || []);
} catch (error) {
console.error('Error fetching transfer type', error);
}
};
useEffect(() => {
fetchTransferType();
}, []);
useEffect(() => {
const fetchWalletTransactions = async () => {
if (!selectedWallet?.ID || !showDetailDialog) return;
setIsLoading(true);
try {
const response = await GetData(
`${API_URL_WALLET}/dashboard/balance/list-balance-detail/${selectedWallet.ID}`,
{
limit: 100,
page: 1,
order_field: 'created_at',
order_direction: 'DESC'
}
);
const transactionData =
response?.data?.list || (Array.isArray(response?.data) ? response.data : []);
// console.log('Loaded Transactions:', transactionData);
setTransactions(transactionData);
setFilteredTransactions(transactionData);
} catch (error) {
console.error('Error fetching balance:', error);
} finally {
setIsLoading(false);
}
};
fetchWalletTransactions();
}, [showDetailDialog, selectedWallet, GetData]);
useEffect(() => {
if (transactions.length === 0) return;
setIsLoading(true);
const timer = setTimeout(() => {
// console.log('Debugging Filter - Selected Transfer Type:', selectedTransferType);
const filtered = transactions.filter((transaction) => {
// console.log('Transaction Type:', {
// id: transaction.transaction_type?.id,
// expected: selectedTransferType,
// match: transaction.transaction_type?.id === selectedTransferType
// });
const transactionDate = new Date(transaction.date);
const fromDate = new Date(dateRange.from);
const toDate = new Date(dateRange.to);
fromDate.setHours(0, 0, 0, 0);
toDate.setHours(23, 59, 59, 999);
const dateMatch = transactionDate >= fromDate && transactionDate <= toDate;
const typeMatch = selectedTransferType
? transaction.transaction_type?.id === selectedTransferType
: true;
const codeMatch = searchValue
? transaction.transaction_code.toLowerCase().includes(searchValue.toLowerCase())
: true;
const categoryMatch = selectedCategory ? transaction.category === selectedCategory : true;
return dateMatch && typeMatch && codeMatch && categoryMatch;
});
// console.log('Filtered Results Count:', filtered.length);
setFilteredTransactions(filtered);
setIsLoading(false);
}, 300);
return () => clearTimeout(timer);
}, [dateRange, transactions, selectedTransferType, searchValue, selectedCategory]);
const handleCategoryChange = (value: string) => {
setSelectedCategory(value);
};
const handleTransferTypeChange = (value: string) => {
// console.log('Transfer Type Changed:', value);
setSelectedTransferType(value === 'all' ? null : value);
};
const handleClearAllFilters = () => {
setDateRange(getDefaultDateRange());
setSelectedTransferType(null);
setSearchValue('');
setSelectedCategory(null);
};
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]);
return (
<Dialog open={showDetailDialog} onOpenChange={setShowDetailDialog}>
<DialogContent className="max-w-[95vw] sm:max-w-[1200px] p-4 overflow-hidden">
<DialogHeader>
<DialogTitle>Details Wallet Statement</DialogTitle>
</DialogHeader>
<DialogBody>
<div className="flex flex-wrap gap-2 lg:gap-5 w-full mb-4">
<div className="flex flex-wrap gap-3 w-full">
<label className="input input-sm w-full sm:w-[160px]">
From
<input
type="date"
placeholder="From"
value={dateRange.from}
onChange={(e) => setDateRange({ ...dateRange, from: e.target.value })}
/>
</label>
<label className="input input-sm w-full sm:w-[160px]">
To
<input
type="date"
placeholder="To"
value={dateRange.to}
onChange={(e) => setDateRange({ ...dateRange, to: e.target.value })}
/>
</label>
<div className="w-full sm:w-[160px]">
<Select
value={selectedTransferType || 'all'}
onValueChange={handleTransferTypeChange}
>
<SelectTrigger className="h-[32px]">
<SelectValue placeholder="Transaction Type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Select Transaction</SelectItem>
{transferType.map((transfer) => (
<SelectItem
key={transfer.id}
value={transfer.id} // Pastikan tidak ada .toString() di sini
>
{transfer.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="w-full sm:w-[160px]">
<Select value={selectedCategory ?? ''} onValueChange={handleCategoryChange}>
<SelectTrigger className="h-[32px]">
<SelectValue placeholder="Select Category" />
</SelectTrigger>
<SelectContent>
{category.length === 0 ? (
<div className="text-gray-500 px-4 py-2">No data</div>
) : (
category.map((cat) => (
<SelectItem key={cat} value={cat}>
{cat}
</SelectItem>
))
)}
</SelectContent>
</Select>
</div>
<label className="input input-sm w-full sm:w-[160px]">
<KeenIcon icon="magnifier" />
<input
type="text"
placeholder="Transaction Code"
className="overflow-hidden text-ellipsis w-full"
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
/>
</label>
<DefaultTooltip title={'Reset Filter'} placement={'top'}>
<Button
variant="outline"
className="h-8 disabled:bg-gray-400"
onClick={handleClearAllFilters}
>
<KeenIcon icon="arrow-circle-left" />
</Button>
</DefaultTooltip>
</div>
</div>
<div className="py-4 max-h-[70vh] overflow-y-auto">
{isLoading ? (
<div className="text-center text-gray-500">Loading details...</div>
) : filteredTransactions.length > 0 ? (
<div className="w-full">
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="bg-gray-100 text-left">
<th className="p-3 text-sm min-w-[120px]">Transaction Code</th>
<th className="p-3 text-sm min-w-[180px]">Transaction Type</th>
<th className="p-3 text-sm min-w-[80px]">Type</th>
<th className="p-3 text-sm min-w-[100px]">Amount</th>
<th className="p-3 text-sm min-w-[100px]">Pre Amount</th>
<th className="p-3 text-sm min-w-[100px]">Post Amount</th>
<th className="p-3 text-sm min-w-[80px]">Category</th>
<th className="p-3 text-sm min-w-[150px]">Date</th>
<th className="p-3 text-sm min-w-[200px]">Notes</th>
</tr>
</thead>
<tbody>
{filteredTransactions.map((transaction) => (
<tr key={transaction.ID}>
<td className="p-3 text-sm">{transaction.transaction_code}</td>
<td className="p-3 text-sm">
{transaction.transaction_type?.name || 'N/A'}
</td>
<td className="p-3 text-sm">{transaction.type}</td>
<td className="p-3 text-sm">{transaction.amount.toFixed(2)}</td>
<td className="p-3 text-sm">{transaction.pre_amount.toFixed(2)}</td>
<td className="p-3 text-sm">{transaction.post_amount.toFixed(2)}</td>
<td className="p-3 text-sm">{transaction.category}</td>
<td className="p-3 text-sm">
{new Date(transaction.date).toLocaleString('id-ID', {
day: '2-digit',
month: 'short',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}
</td>
<td className="p-3 text-sm">{transaction.notes || '-'}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
) : (
<div className="text-center text-gray-500 py-10">No transaction data available</div>
)}
</div>
</DialogBody>
</DialogContent>
</Dialog>
);
};
export default ShowDialog;

View File

@ -5,6 +5,7 @@ import { useCallApi } from '@/hooks';
import { ColumnDef } from '@tanstack/react-table'; import { ColumnDef } from '@tanstack/react-table';
import React, { createContext, useCallback, useMemo, useState } from 'react'; import React, { createContext, useCallback, useMemo, useState } from 'react';
import ListToolbar from '../blocks/ListToolbar'; import ListToolbar from '../blocks/ListToolbar';
import ShowDetailDialog from '../blocks/ShowDetailDialog';
const formatNumber = (num: number): string => { const formatNumber = (num: number): string => {
return num.toLocaleString('en-US', { return num.toLocaleString('en-US', {
@ -14,7 +15,9 @@ const formatNumber = (num: number): string => {
}; };
interface WalletProps { interface WalletProps {
ID: string;
id: string; id: string;
id_wallet: string;
name: string; name: string;
id_currency: string; id_currency: string;
status: string; status: string;
@ -22,6 +25,8 @@ interface WalletProps {
interface ContextProps { interface ContextProps {
wallets: WalletProps[]; wallets: WalletProps[];
showDetailDialog: boolean;
setShowDetailDialog: React.Dispatch<React.SetStateAction<boolean>>;
showAddDialog: boolean; showAddDialog: boolean;
handleAddDialog: (show: boolean) => void; handleAddDialog: (show: boolean) => void;
showEditDialog: boolean; showEditDialog: boolean;
@ -39,6 +44,8 @@ interface ContextProps {
const initialProps: ContextProps = { const initialProps: ContextProps = {
wallets: [], wallets: [],
showDetailDialog: false,
setShowDetailDialog: () => {},
showAddDialog: false, showAddDialog: false,
handleAddDialog: (show: boolean) => {}, handleAddDialog: (show: boolean) => {},
showEditDialog: false, showEditDialog: false,
@ -54,6 +61,7 @@ const API_URL_WALLET = apiConfig.service_wallet;
const ManageWalletContextProvider = ({ children }: { children: React.ReactNode }) => { const ManageWalletContextProvider = ({ children }: { children: React.ReactNode }) => {
const [wallets, setWallets] = useState<WalletProps[]>([]); const [wallets, setWallets] = useState<WalletProps[]>([]);
const [showDetailDialog, setShowDetailDialog] = useState(false);
const [showAddDialog, setShowAddDialog] = useState(false); const [showAddDialog, setShowAddDialog] = useState(false);
const [showEditDialog, setShowEditDialog] = useState(false); const [showEditDialog, setShowEditDialog] = useState(false);
const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [showDeleteDialog, setShowDeleteDialog] = useState(false);
@ -92,7 +100,7 @@ const ManageWalletContextProvider = ({ children }: { children: React.ReactNode }
enableSorting: false, enableSorting: false,
enableHiding: false, enableHiding: false,
meta: { meta: {
headerClassName: 'w-[550px]', headerClassName: 'w-[300px]',
cellClassName: 'p-[20px]' cellClassName: 'p-[20px]'
} }
}, },
@ -106,6 +114,26 @@ const ManageWalletContextProvider = ({ children }: { children: React.ReactNode }
headerClassName: 'w-[200px]' headerClassName: 'w-[200px]'
} }
}, },
{
accessorKey: 'total_debit',
header: ({ column }) => <DataGridColumnHeader title="Total Debit" column={column} />,
cell: ({ row }) => formatNumber(row.original.total_debit),
enableSorting: false,
enableHiding: false,
meta: {
headerClassName: 'w-[200px]'
}
},
{
accessorKey: 'total_credit',
header: ({ column }) => <DataGridColumnHeader title="Total Credit" column={column} />,
cell: ({ row }) => formatNumber(row.original.total_credit),
enableSorting: false,
enableHiding: false,
meta: {
headerClassName: 'w-[200px]'
}
},
{ {
accessorKey: 'trx_count_today', accessorKey: 'trx_count_today',
header: ({ column }) => ( header: ({ column }) => (
@ -144,16 +172,6 @@ const ManageWalletContextProvider = ({ children }: { children: React.ReactNode }
headerClassName: 'w-[200px]' headerClassName: 'w-[200px]'
} }
}, },
// {
// accessorFn: (row) => row.balance_type?.name,
// id: 'balance_type_name',
// header: ({ column }) => <DataGridColumnHeader title="Balance Type Name" column={column} />,
// enableSorting: false,
// enableHiding: false,
// meta: {
// headerClassName: 'w-[200px]'
// }
// },
{ {
accessorKey: 'currency_name', accessorKey: 'currency_name',
header: ({ column }) => <DataGridColumnHeader title="Currency Name" column={column} />, header: ({ column }) => <DataGridColumnHeader title="Currency Name" column={column} />,
@ -182,6 +200,32 @@ const ManageWalletContextProvider = ({ children }: { children: React.ReactNode }
headerClassName: 'w-[200px]' headerClassName: 'w-[200px]'
} }
}, },
{
accessorKey: 'detail_statemenet',
header: ({ column }) => <DataGridColumnHeader title="Detail Statement" column={column} />,
enableSorting: false,
enableHiding: false,
cell: (data) => {
const row = data.row.original;
return (
<div key={`detail_statement-${row.id}`} className="flex gap-2 justify-center">
<button
className="btn btn-sm btn-icon btn-clear btn-light"
onClick={() => {
setSelectedWallet({
...row,
ID: row.ID || row.id
});
setShowDetailDialog(true);
}}
>
{' '}
<KeenIcon icon="eye" />
</button>
</div>
);
}
}
// { // {
// accessorFn: (row) => row.group?.is_bank, // accessorFn: (row) => row.group?.is_bank,
// id: 'is_bank', // id: 'is_bank',
@ -219,7 +263,7 @@ const ManageWalletContextProvider = ({ children }: { children: React.ReactNode }
if (f.id === 'id_wallet' && f.value) { if (f.id === 'id_wallet' && f.value) {
filterParams.id_wallet = f.value; filterParams.id_wallet = f.value;
} }
if (f.id === 'id_group' && f.value) { if (f.id === 'id_group' && f.value) {
filterParams.id_group = f.value; filterParams.id_group = f.value;
} }
@ -325,6 +369,8 @@ const ManageWalletContextProvider = ({ children }: { children: React.ReactNode }
<ManageWalletContext.Provider <ManageWalletContext.Provider
value={{ value={{
wallets, wallets,
showDetailDialog,
setShowDetailDialog,
showAddDialog, showAddDialog,
handleAddDialog, handleAddDialog,
showEditDialog, showEditDialog,
@ -336,6 +382,9 @@ const ManageWalletContextProvider = ({ children }: { children: React.ReactNode }
}} }}
> >
<Toaster expand visibleToasts={9} duration={3000} /> <Toaster expand visibleToasts={9} duration={3000} />
<ShowDetailDialog />
<DataGridProvider <DataGridProvider
columns={columns} columns={columns}
pagination={{ size: 10 }} pagination={{ size: 10 }}
@ -353,4 +402,4 @@ const ManageWalletContextProvider = ({ children }: { children: React.ReactNode }
}; };
export { ManageWalletContext, ManageWalletContextProvider }; export { ManageWalletContext, ManageWalletContextProvider };
export type { WalletProps }; export type { WalletProps };