update detail wallet statement

This commit is contained in:
Raja Oktafrianto
2025-06-04 22:33:28 +07:00
parent 14eafd409d
commit 680ca8cb70
4 changed files with 319 additions and 269 deletions

View File

@ -0,0 +1,219 @@
import React, { useCallback, useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from '@/components/ui/select';
import { DefaultTooltip, KeenIcon } from '@/components';
import { toAbsoluteUrl } from '@/utils';
import { useCallApi } from '@/hooks';
import { apiConfig } from '@/config/api.config';
interface TransferType {
id: string;
name: string;
}
interface FilterSectionProps {
filters: any;
setFilters: (filters: any) => void;
transaction: any[];
categoryMap: Record<string, string>;
}
const FilterSection: React.FC<FilterSectionProps> = ({
filters,
setFilters,
transaction,
categoryMap
}) => {
const { GetData } = useCallApi();
const [dateRange, setDateRange] = useState({
from: new Date(new Date().getTime() - 7 * 24 * 60 * 60 * 1000).toLocaleDateString('sv-SE'),
to: new Date().toLocaleDateString('sv-SE')
});
const [selectedTransferType, setSelectedTransferType] = useState<string | null>(null);
const [transferType, setTransferType] = useState<TransferType[]>([]);
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
const [category, setCategory] = useState<string[]>([]);
const [searchValue, setSearchValue] = useState('');
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]);
useEffect(() => {
fetchTransferType();
}, [fetchTransferType]);
const categories = Array.from(
new Set(
transaction.map((tx) => categoryMap[tx.category] || '_').filter((cat) => !!cat && cat !== '_')
)
);
useEffect(() => {
setCategory(categories);
}, [transaction, categoryMap]);
const reverseCategoryMap = Object.fromEntries(
Object.entries(categoryMap).map(([key, value]) => [value, key])
);
const handleTransferTypeChange = (value: string) => {
setSelectedTransferType(value === 'all' ? null : value);
};
const handleCategoryChange = (value: string) => {
setSelectedCategory(value === '' ? null : value);
};
const handleClearAllFilters = () => {
setDateRange({
from: new Date(new Date().getTime() - 7 * 24 * 60 * 60 * 1000).toLocaleDateString('sv-SE'),
to: new Date().toLocaleDateString('sv-SE')
});
setSelectedTransferType(null);
setSelectedCategory(null);
setSearchValue('');
setFilters({});
};
const handleApplyFilters = () => {
const appliedFilters: any = {};
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()
};
}
if (selectedTransferType) {
appliedFilters.id_transaction_type = selectedTransferType;
}
if (selectedCategory) {
appliedFilters.category = selectedCategory;
}
if (searchValue) {
appliedFilters.transaction_code = searchValue;
}
setFilters(appliedFilters);
};
return (
<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"
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"
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}>
{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={reverseCategoryMap[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" onClick={handleClearAllFilters}>
<KeenIcon icon="arrow-circle-left" />
</Button>
</DefaultTooltip>
<Button variant="default" className="h-8" onClick={handleApplyFilters}>
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>
<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>
</div>
</div>
);
};
export default FilterSection;

View File

@ -111,8 +111,8 @@ const ListToolbar = ({
};
useEffect(() => {
table.setPageIndex(0); // reset ke halaman pertama kalau mau
reload(); // ini yang akan trigger data fetch ulang
table.setPageIndex(0);
reload();
}, [filters]);
return (
@ -171,7 +171,7 @@ const ListToolbar = ({
</Button>
</DefaultTooltip>
<Button variant="outline" className="h-8" onClick={handleApplyFilters}>
<Button variant="default" className="h-8" onClick={handleApplyFilters}>
Filter Data
</Button>
</div>

View File

@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useState } from 'react';
import React, { createContext, useCallback, useEffect, useState } from 'react';
import {
Dialog,
DialogBody,
@ -10,63 +10,26 @@ import {
import { useCallApi } from '@/hooks';
import { apiConfig } from '@/config/api.config';
import { DataGridColumnHeader, DataGridProvider } from '@/components';
import { Button } from '@/components/ui/button';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from '@/components/ui/select';
import { DefaultTooltip, KeenIcon } from '@/components';
import { useManageStatementContext } from '../hooks/useManageWalletStatementContext';
import { Toaster } from 'sonner';
import { format } from 'date-fns';
import FilterSection from './FilterSection';
interface TransferType {
id: string;
name: string;
interface ContextProps {
doExportData: (sorting: any, filter: any) => Promise<any>;
}
const formatDate = (date: Date) => date.toLocaleDateString('sv-SE');
const getDefaultDateRange = () => {
const today = new Date();
const sevenDaysAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000);
return {
from: formatDate(sevenDaysAgo),
to: formatDate(today)
};
const initialProps: ContextProps = {
doExportData: async () => ({ data: [], totalCount: 0 })
};
export const showDetailWalletContext = createContext<ContextProps>(initialProps);
const ShowDetailWalletDialog = () => {
const { GetData } = useCallApi();
const { GetData, GetExportData } = useCallApi();
const { showDetailDialog, setShowDetailDialog, selectedWallet } = useManageStatementContext();
const [dateRange, setDateRange] = useState(getDefaultDateRange());
const [selectedTransferType, setSelectedTransferType] = useState<string | null>(null);
const [transferType, setTransferType] = useState<TransferType[]>([]);
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
const [searchValue, setSearchValue] = useState('');
const [category, setCategory] = useState<string[]>([]);
const [transaction, setTransaction] = useState<any[]>([]);
const [filters, setFilters] = useState<any>({});
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'
});
(response?.data?.list || []).sort((a: any, b: any) => a.name.localeCompare(b.name));
} catch (error) {
console.error('Error fetching transfer types', error);
}
}, [GetData]);
useEffect(() => {
fetchTransferType();
}, [fetchTransferType]);
const [transaction, setTransaction] = useState<any[]>([]);
const categoryMap: Record<string, string> = {
T: 'TRANSFER',
@ -90,65 +53,6 @@ const ShowDetailWalletDialog = () => {
H: 'CASHBACK POINT'
};
useEffect(() => {
const uniqueCategories = Array.from(
new Set(
transaction
.map((tx) => categoryMap[tx.category] || '_')
.filter((cat) => !!cat && cat !== '_')
)
);
setCategory(uniqueCategories);
}, [transaction]);
const reverseCategoryMap = Object.fromEntries(
Object.entries(categoryMap).map(([key, value]) => [value, key])
);
const handleTransferTypeChange = (value: string) => {
setSelectedTransferType(value === 'all' ? null : value);
};
const handleCategoryChange = (value: string) => {
setSelectedCategory(value === '' ? null : value);
};
const handleClearAllFilters = () => {
setDateRange(getDefaultDateRange());
setSelectedTransferType(null);
setSelectedCategory(null);
setSearchValue('');
setFilters({});
};
const handleApplyFilters = () => {
const appliedFilters: any = {};
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()
};
}
if (selectedTransferType) {
appliedFilters.transaction_type_id = selectedTransferType;
}
if (selectedCategory) {
appliedFilters.category = selectedCategory;
}
if (searchValue) {
appliedFilters.transaction_code = searchValue;
}
setFilters(appliedFilters);
};
const columns = [
{
accessorKey: 'transaction_code',
@ -188,7 +92,7 @@ const ShowDetailWalletDialog = () => {
{
accessorKey: 'amount',
header: ({ column }: any) => <DataGridColumnHeader title="Amount" column={column} />,
enableSorting: false,
enableSorting: true,
cell: (info: any) => info.getValue()?.toFixed(2),
meta: { headerClassName: 'min-w-[100px]' }
},
@ -200,57 +104,12 @@ const ShowDetailWalletDialog = () => {
meta: { headerClassName: 'min-w-[100px]' }
},
{
accessorFn: (row: any) => {
switch (row.category) {
case 'T':
return 'TRANSFER';
case 'P':
return 'PURCHASE';
case 'W':
return 'WITHDRAW';
case 'U':
return 'TOP UP';
case 'R':
return 'RETURN';
case 'N':
return 'TOP UP PARTNER';
case 'E':
return 'REWARD';
case 'L':
return 'PURCHASE LOJA';
case 'B':
return 'TOP UP P24';
case 'A':
return 'TRANSFER AGENT';
case 'M':
return 'WITHDRAWAL AGENT';
case 'O':
return 'TOP UP AGENT';
case 'S':
return 'TRANSFER P24';
case 'I':
return 'WIJTDRAW MERCHANT';
case 'D':
return 'DONATION';
case 'F':
return 'FEE';
case 'V':
return 'REVERSAL';
case 'C':
return 'CASHBACK CASH';
case 'H':
return 'CASHBACK POINT';
default:
return '_';
}
},
accessorFn: (row: any) => categoryMap[row.category] || '_',
accessorKey: 'category',
header: ({ column }: any) => <DataGridColumnHeader title="Category" column={column} />,
enableSorting: false,
enableHiding: false,
meta: {
headerClassName: 'w-[250px]'
}
meta: { headerClassName: 'w-[250px]' }
},
{
accessorKey: 'date',
@ -294,14 +153,12 @@ const ShowDetailWalletDialog = () => {
limit,
page: page + 1,
with_deleted: false,
order_field: 'created_at',
order_direction: 'DESC',
order_field: sorting?.[0]?.id ?? 'amount',
order_direction: sorting?.[0]?.desc ? 'ASC' : 'DESC',
filter: JSON.stringify(filters)
}
);
// console.log(response);
if (!response || !response.data) {
console.warn('No data received:', response);
return { data: [], totalCount: 0 };
@ -317,7 +174,53 @@ const ShowDetailWalletDialog = () => {
[GetData, filters, selectedWallet]
);
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}`
);
const parsedUser = user ? JSON.parse(user) : null;
sorting = sorting.length === 0 ? [{ id: 'created_at', desc: false }] : sorting;
filter = filter && filter.length > 0 ? filter : [];
let dataFilter: any = {};
if (filter && filter.length > 0) {
filter.forEach((f: any) => {
if (f.id === 'username') {
dataFilter['username'] = { like: f.value };
} else if (f.id === 'created_at') {
dataFilter['created_at'] = f.value;
}
});
}
if (!dataFilter['created_at']) {
const defaultFrom = new Date(new Date().setMonth(new Date().getMonth() - 1));
const defaultTo = new Date();
dataFilter['created_at'] = {
from: `${format(defaultFrom, 'yyyy-MM-dd')} 00:00:00`,
to: `${format(defaultTo, 'yyyy-MM-dd')} 23:59:59`
};
}
let param = {
order_field: sorting[0].id || 'created_at',
order_direction: sorting[0].desc ? 'DESC' : 'ASC',
with_deleted: false,
filter: JSON.stringify(dataFilter),
token: parsedUser?.access_token
};
let url = `${apiConfig.service_wallet}/dashboard/balance/list-balance-download/${selectedWallet?.ID}`;
GetExportData(url, param, 'user_activity_export_');
};
return (
<showDetailWalletContext.Provider value={{ doExportData }}>
<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">
<DialogHeader>
@ -326,86 +229,12 @@ const ShowDetailWalletDialog = () => {
<DialogDescription />
<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"
value={dateRange.from}
onChange={(e) => setDateRange({ ...dateRange, from: e.target.value })}
<FilterSection
filters={filters}
setFilters={setFilters}
transaction={transaction}
categoryMap={categoryMap}
/>
</label>
<label className="input input-sm w-full sm:w-[160px]">
To
<input
type="date"
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}>
{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={reverseCategoryMap[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" onClick={handleClearAllFilters}>
<KeenIcon icon="arrow-circle-left" />
</Button>
</DefaultTooltip>
<Button variant="outline" className="h-8" onClick={handleApplyFilters}>
Filter Data
</Button>
</div>
</div>
<div className="py-4 max-h-[70vh] overflow-y-auto">
<DataGridProvider
@ -413,7 +242,7 @@ const ShowDetailWalletDialog = () => {
columns={columns}
pagination={{ size: 5 }}
layout={{ card: true }}
sorting={[{ id: 'id', desc: false }]}
sorting={[{ id: 'amount', desc: false }]}
serverSide={true}
onFetchData={({ pageIndex, pageSize, sorting, columnFilters }) =>
getTransactionLists(pageIndex, pageSize, sorting, columnFilters)
@ -423,6 +252,7 @@ const ShowDetailWalletDialog = () => {
</DialogBody>
</DialogContent>
</Dialog>
</showDetailWalletContext.Provider>
);
};

View File

@ -108,7 +108,7 @@ const ManageWalletContextProvider = ({ children }: { children: React.ReactNode }
accessorKey: 'amount',
header: ({ column }) => <DataGridColumnHeader title="Amount" column={column} />,
cell: ({ row }) => formatNumber(row.original.amount),
enableSorting: false,
enableSorting: true,
enableHiding: false,
meta: {
headerClassName: 'w-[200px]'
@ -242,7 +242,7 @@ const ManageWalletContextProvider = ({ children }: { children: React.ReactNode }
const getWalletLists = async (page: number, limit: number, sorting: any, filter: any) => {
try {
const sortField = sorting.length > 0 ? sorting[0].id : 'created_at';
const sortField = sorting.length > 0 ? sorting[0].id : 'amount';
const sortDirection = sorting.length > 0 ? (sorting[0].desc ? 'ASC' : 'DESC') : 'DESC';
let filterParams: any = {};
@ -383,10 +383,11 @@ const ManageWalletContextProvider = ({ children }: { children: React.ReactNode }
pagination={{ size: 10 }}
toolbar={<ListToolbar filters={filters} setFilters={setFilters} />}
layout={{ card: true }}
sorting={[{ id: 'amount', desc: false}]}
serverSide={true}
onFetchData={
({ pageIndex, pageSize, sorting }) =>
getWalletLists(pageIndex, pageSize, sorting, filters) // gunakan filters dari state, bukan columnFilters
getWalletLists(pageIndex, pageSize, sorting, filters)
}
>
{children}