This commit is contained in:
Raja Oktafrianto
2025-05-14 20:05:31 +07:00
20 changed files with 701 additions and 72 deletions

View File

@ -43,6 +43,7 @@ const TransactionPieChart = ({ startdate, enddate }: Props) => {
{ name: 'Purchase', value: parseFloat((responseTransactionValue?.data?.P ?? 0).toFixed(2)), color: '#baf7c5' },
{ name: 'Withdraw', value: parseFloat((responseTransactionValue?.data?.W ?? 0).toFixed(2)), color: '#f56565' },
{ name: 'Top Up Partner', value: parseFloat((responseTransactionValue?.data?.N ?? 0).toFixed(2)), color: '#f7fa52' },
{ name: 'Reward', value: parseFloat((responseTransactionValue?.data?.E ?? 0).toFixed(2)), color: '#f7fa52' },
];
return (

View File

@ -64,6 +64,9 @@ const TransactionValue = ({ startdate, enddate }: Props) => {
case "N":
type = "Top Up Partner";
break;
case "E":
type = "Reward";
break;
default:
type = "Unknown";
break;

View File

@ -0,0 +1,236 @@
import { DataTable } from '@/components/ui/DataTable';
import { getColumns } from './Column';
import { apiConfig } from '@/config/api.config';
import axios, { AxiosResponse } from 'axios';
import { Helmet } from 'react-helmet';
import {
DialogContent,
MenuItem,
Radio,
RadioGroup,
FormControlLabel,
FormControl,
Dialog,
DialogTitle,
Typography,
Button,
Box,
Breadcrumbs,
Link
} from '@mui/material';
import { useState, useEffect } from 'react';
import CloseIcon from '@mui/icons-material/Close';
import Divider from '@mui/material/Divider';
import ConfirmDialog from '@/components/confirm';
import { toast } from 'sonner';
import { Container, DataGridLoader, DataGridProvider, LoaderTransparant } from '@/components';
import { ListToolBar } from './ListToolbar';
import { RefreshCw } from 'lucide-react';
import { setgroups } from 'process';
const BASE_URL = apiConfig.service_customer;
let initBalance = {
id: '',
msisdn: '',
fullname: '',
username: '',
agent_name: '',
balance_emoney: '',
balance_merchant: '',
balance_point: '',
balance_deposit: ''
};
const AgentBalance = () => {
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [dataBalance, setDataBalance] = useState([]);
const [formData, setFormData] = useState(initBalance);
const [pageIndex, setPageIndex] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [dialogType, setDialogType] = useState('');
const [dialogOpen, setDialogOpen] = useState(false);
const [isReloading, setIsReloading] = useState(false);
useEffect(() => {
fetchAgentBalance();
}, []);
async function fetchAgentBalance(): Promise<void> {
setIsReloading(true);
try {
let balances = await axios.get(`${BASE_URL}/customer/agent-balance`, {
params: {
limit: 20,
page: 1,
with_deleted: false,
order_field: 'created_at',
order_direction: 'DESC'
}
});
let temp = 1;
let resBalance = balances.data.data.list.map((el: any) => {
el.no = temp++;
return el;
});
setDataBalance(resBalance);
} catch (error: any) {
alert(error.message);
console.log(error);
} finally {
setIsReloading(false);
}
}
const openDialog = () => setIsDialogOpen(true);
const closeDialog = () => {
setIsDialogOpen(false);
setFormData(initBalance);
};
function createGroup() {
setDialogType('create');
openDialog();
}
const handleUpdate = (group: any) => {
setDialogType('update');
setIsDialogOpen(true);
};
const handleReload = () => {
setIsReloading(true);
fetchAgentBalance();
};
if (loading) return <LoaderTransparant />;
return (
<>
<Helmet>
<title>TPAY | Agent Balance</title>
</Helmet>
<Container>
{/* <ConfirmDialog
open={dialogOpen}
onClose={() => setDialogOpen(false)}
title="Confirm Action"
content={''}
onYes={handleYes}
onNo={() => setDialogOpen(false)}
/> */}
<h1 className="text-xl font-medium leading-none text-gray-900 mb-5">Agent Balances</h1>
<Breadcrumbs>
<Link underline="none" color="inherit" href="/">
<span className="text-sm hover:underline">Dashboard</span>
</Link>
<Link underline="none" color="inherit">
<span className="text-sm">Members</span>
</Link>
<Link underline="none" color="inherit">
<span className="text-sm">Agent Balances</span>
</Link>
</Breadcrumbs>
<div className="grid gap-5 lg:gap-7.5 mt-5 relative">
{isReloading && (
<DataGridLoader />
)}
<DataGridProvider
data={dataBalance}
columns={getColumns(handleUpdate)}
pagination={{ size: 10 }}
toolbar={
<ListToolBar
createGroup={createGroup}
onReload={handleReload}
isReloading={isReloading}
/>
}
layout={{ card: true }}
serverSide={false}
onRowSelectionChange={(selected, table: any) => {
const selectedRow = table.getSelectedRowModel().rows[0];
if (selectedRow) handleUpdate(selectedRow.original);
}}
/>
</div>
{/* </div> */}
{/* <Dialog open={isDialogOpen} onClose={closeDialog}>
<DialogContent className="w-full">
<div className="flex justify-between">
<DialogTitle>
{dialogType === 'create' ? 'Create New Group' : 'Update Group'}
</DialogTitle>
<Box display="flex" justifyContent="flex-end">
<Button
variant="outlined"
sx={{ borderColor: 'white', color: 'grey' }}
onClick={closeDialog}
>
<CloseIcon />
</Button>
</Box>
</div>
<Divider />
<div className="p-5 mt-5">
<form onSubmit={handleSubmit} className="flex flex-col gap-4 w-full">
<div className="grid grid-cols-4 items-center gap-4 w-full">
<label className="form-label text-sm">
<span className="text-red-500">*</span>Group Name:
</label>
<input
type="text"
name="groupName"
className="input w-full col-span-3"
value={formData.groupName}
onChange={handleChange}
/>
</div>
<div className="grid grid-cols-4 items-center gap-4 w-full">
<label className="form-label text-sm">
<span className="text-red-500">*</span>Active Status:
</label>
<FormControl>
<RadioGroup name="status" row value={formData.status} onChange={handleChange}>
<FormControlLabel
value="Y"
checked={formData.status === 'Y'}
control={<Radio />}
label="Yes"
/>
<FormControlLabel
value="N"
checked={formData.status === 'N'}
control={<Radio />}
label="No"
/>
</RadioGroup>
</FormControl>
</div>
<div className="grid grid-cols-4 items-center gap-4 w-full">
<label className="form-label text-sm">
<span className="text-red-500">*</span>Description:
</label>
<input
type="text"
name="description"
className="input w-full col-span-3"
value={formData.description}
onChange={handleChange}
/>
</div>
<Button type="submit">Submit</Button>
</form>
</div>
</DialogContent>
</Dialog> */}
</Container>
</>
);
};
export default AgentBalance;

View File

@ -0,0 +1,146 @@
import { KeenIcon } from '@/components';
import { Button } from '@/components/ui/button';
import { ColumnDef } from '@tanstack/react-table';
import { ArrowUpDown } from 'lucide-react';
import moment from 'moment';
export type AgentBalance = {
id: string,
msisdn: string,
fullname: string,
username: string,
agent_name: string,
balance_emoney: string,
balance_merchant: string,
balance_point: string,
balance_deposit: string
};
export const getColumns = (handleUpdate: (data: any) => void): ColumnDef<AgentBalance>[] => [
{
accessorKey: 'no',
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
>
No.
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
}
},
{
accessorKey: 'msisdn',
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
>
Phone Number
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
}
},
{
accessorKey: 'fullname',
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
>
Fullname
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
}
},
{
accessorKey: 'username',
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
>
Username
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
}
},
{
accessorKey: 'agent_name',
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
>
Merchant Name
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
}
},
{
accessorKey: 'balance_emoney',
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
>
E-money
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
}
},
{
accessorKey: 'balance_merchant',
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
>
Merchant
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
}
},
{
accessorKey: 'balance_point',
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
>
Point
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
}
},
{
accessorKey: 'balance_deposit',
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
>
Deposit
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
}
},
];

View File

@ -0,0 +1,82 @@
import { DefaultTooltip, KeenIcon, useDataGrid } from '@/components';
import { Button } from '@/components/ui/button';
import React, { useState } from 'react';
interface ListToolBarProps {
createGroup: () => void;
onReload: () => void;
isReloading: boolean;
}
const ListToolBar = ({ createGroup, onReload, isReloading }: ListToolBarProps) => {
const { table } = useDataGrid();
const [usernameFilter, setUsernameFilter] = useState('');
const [msisdn, setMsisdn] = useState('');
const [fullname, setFullname] = useState('');
const [merchantName, setMerchantName] = useState('');
const handleUsernameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setUsernameFilter(e.target.value);
table.getColumn('username')?.setFilterValue(e.target.value);
};
const filtermsisdn = (e: React.ChangeEvent<HTMLInputElement>) => {
setMsisdn(e.target.value);
table.getColumn('msisdn')?.setFilterValue(e.target.value);
};
const filterfullname = (e: React.ChangeEvent<HTMLInputElement>) => {
setFullname(e.target.value);
table.getColumn('fullname')?.setFilterValue(e.target.value);
};
const filtermerchantname = (e: React.ChangeEvent<HTMLInputElement>) => {
setMerchantName(e.target.value);
table.getColumn('agent_name')?.setFilterValue(e.target.value);
};
return (
<div className="card-header flex-wrap gap-2 border-b-0 px-5">
<div className="flex flex-wrap gap-2 lg:gap-5 w-full">
<div className="flex justify-between w-full items-center">
<div className="flex w-[50%] gap-3 items-center">
<label className="input input-sm w-1/3">
<KeenIcon icon="magnifier" />
<input type="text" placeholder="Search phone" value={msisdn} onChange={filtermsisdn}/>
</label>
<label className="input input-sm w-1/3">
<KeenIcon icon="magnifier" />
<input type="text" placeholder="Search fullname" value={fullname} onChange={filterfullname}/>
</label>
<label className="input input-sm w-1/3">
<KeenIcon icon="magnifier" />
<input type="text" placeholder="Search username" value={usernameFilter} onChange={handleUsernameChange}/>
</label>
<label className="input input-sm w-1/3">
<KeenIcon icon="magnifier" />
<input type="text" placeholder="Search merchant name" value={merchantName} onChange={filtermerchantname}/>
</label>
</div>
<div className="flex gap-3 items-center">
{/* <Button variant="outline" className="h-7.5 text-[0.8rem]" onClick={createGroup}>
Add Data
</Button> */}
<DefaultTooltip title={isReloading ? 'Refreshing...' : 'Refresh'} placement={'top'}>
<Button variant="outline" className="h-7.5" onClick={onReload} disabled={isReloading}>
{isReloading ? (
<div className="animate-spin">
<KeenIcon icon="arrows-circle" />
</div>
) : (
<KeenIcon icon="arrows-circle" />
)}
</Button>
</DefaultTooltip>
</div>
</div>
</div>
</div>
);
};
export { ListToolBar };

View File

@ -18,15 +18,37 @@ import { useManageKycDeletionContext } from '../hooks';
import { apiConfig } from '@/config/api.config';
import { useDataGrid } from '@/components';
import { useState } from 'react';
import ConfirmDialog from '@/components/confirm';
const API_URL = apiConfig.service_customer;
const DetailDialog = () => {
const { showDetailDialog, setShowDetailDialog, detailKyc, handleApproveReject } = useManageKycDeletionContext();
const [genders] = useState([ { name: 'Male',id: 'M' }, { name: 'Female',id: 'F' }])
const [dialogOpen, setDialogOpen] = useState(false);
const [dialogType, setDialogType] = useState('');
const { reload } = useDataGrid();
async function handleYes() {
await handleApproveReject(detailKyc.id, dialogType)
setDialogOpen(false)
reload()
}
function onSubmit(type:string) {
setDialogType(type)
setDialogOpen(true)
}
return (
<Dialog open={showDetailDialog} onOpenChange={setShowDetailDialog}>
<ConfirmDialog
open={dialogOpen}
onClose={() => setDialogOpen(false)}
title="Confirm Action"
content={`Are you sure you want to ${dialogType === 'Y' ? 'Approve' : 'Reject'}?`}
onYes={handleYes}
onNo={() => setDialogOpen(false)}
/>
<DialogContent className="container-fixed max-w-[1024px] flex flex-col p-5 overflow-hidden max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Customer Deletion Details </DialogTitle>
@ -49,13 +71,15 @@ const DetailDialog = () => {
<div className="flex justify-end gap-2 mt-3">
<Button type="button" variant="outline" onClick={() => setShowDetailDialog(false)}>Cancel</Button>
<Button onClick={async() => {
await handleApproveReject(detailKyc.id, 'N')
reload()
}} variant="destructive" color="warning" disabled={detailKyc.status_approve==='W'?false:true}>Reject</Button>
// await handleApproveReject(detailKyc.id, 'N')
// reload()
onSubmit('N')
}} variant="destructive" color="warning" disabled={detailKyc.status_approve==='Waiting Approval'?false:true}>Reject</Button>
<Button onClick={async() => {
await handleApproveReject(detailKyc.id, 'Y')
reload()
}} variant="default" color="primary" disabled={detailKyc.status_approve==='W'?false:true}>Approve</Button>
// await handleApproveReject(detailKyc.id, 'Y')
// reload()
onSubmit('Y')
}} variant="default" color="primary" disabled={detailKyc.status_approve==='Waiting Approval'?false:true}>Approve</Button>
</div>
</div>
) : (<div></div>)}

View File

@ -159,7 +159,8 @@ const ManageKycDeletionContextProvider = ({ children }: { children: React.ReactN
})
if (approveReject?.status == true) {
toast.success('Success update status approval')
if (status_approve === 'Y') toast.success('Update status approve success')
if (status_approve === 'N') toast.success('Update status reject success')
handleDetailDialog(false, null)
} else {
toast.warning(`${approveReject?.message}`)

View File

@ -18,7 +18,7 @@ const BASE_URL_CUSTOMER = apiConfig.service_customer;
const BASE_URL = apiConfig.service_customer;
const ManageMembers = () => {
const [loading, setLoading] = useState(false);
// const [loading, setLoading] = useState(false);
const [members, setMembers] = useState([]);
const [selectedMember, setSelectedMember] = useState('');
const [member, setMember] = useState(initialMember);
@ -28,6 +28,9 @@ const ManageMembers = () => {
const [dialogType, setDialogType] = useState('');
const [groups, setGroups] = useState([]);
const [isReloading, setIsReloading] = useState(false);
const [currentPage, setCurrentPage] = useState(0);
const [totalItems, setTotalItems] = useState(10);
const closeDialog = () => {
setIsDialogOpen(false);
setMember(initialMember);
@ -36,6 +39,7 @@ const ManageMembers = () => {
useEffect(() => {
fetchCustomers();
fetchMasters();
}, []);
async function fetchCustomers(): Promise<void> {
@ -44,7 +48,7 @@ const ManageMembers = () => {
let customers = await axios.get(`${BASE_URL}/customer/list`, {
params: {
limit: 30,
page: 1,
page: 1 + currentPage,
with_deleted: false,
order_field: 'created_at',
order_direction: 'DESC'
@ -61,8 +65,17 @@ const ManageMembers = () => {
return el;
});
setMembers(resMembers);
// setIsReloading(false);
} catch (error: any) {
toast.error(error.message);
console.log(error);
} finally {
setIsReloading(false);
}
}
async function fetchMasters(): Promise<void> {
setIsReloading(true);
try {
let getProfession: any = await axios.get(`${BASE_URL_MASTER_DATA}/profession/list`, {
params: {
limit: 50,
@ -87,7 +100,6 @@ const ManageMembers = () => {
toast.error(error.message);
console.log(error);
} finally {
// setLoading(false);
setIsReloading(false);
}
}
@ -269,7 +281,7 @@ const ManageMembers = () => {
)}
<DataGridProvider
data={members}
pagination={{ size: 10 }}
pagination={{ size: totalItems }}
columns={getColumns(handleUpdate)}
layout={{ card: true }}
serverSide={false}

View File

@ -39,9 +39,9 @@ const DetailMember = ({ showAddDialog, setShowAddDialog, handleSubmit, initialDa
const [sucos, setSucos] = useState<any>([]);
const [groups, setGroups] = useState([]);
const [identity_type] = useState([
{ id: 'eleitoral_id', name: 'Eleitoral ID' },
{ id: 'bihete_de_identidade', name: 'Bihete de Identidade' },
{ id: 'passport', name: 'Passport' },
{ id: 'Eleitoral ID', name: 'Eleitoral ID' },
{ id: 'Bihete de Identidade', name: 'Bihete de Identidade' },
{ id: 'Passport', name: 'Passport' },
]);
const [genders] = useState([ { name: 'Male',id: 'M' }, { name: 'Female',id: 'F' }])
const [previewImg, setPreviewImg] = useState({
@ -162,8 +162,8 @@ const DetailMember = ({ showAddDialog, setShowAddDialog, handleSubmit, initialDa
function buttonOnSubmit(e:any) {
e.preventDefault();
if (dialogType === 'update') {
if (page === 'kyc' && formData.destinationGroup === "Premium" && !formData.approval_description_premium) return toast.warning(`Approval Premium Description needed!`)
if (page === 'kyc' && formData.destinationGroup === "Agent" && !formData.approval_description_premium) return toast.warning(`Approval Agent Description needed!`)
// if (page === 'kyc' && formData.destinationGroup === "Premium" && !formData.approval_description_premium) return toast.warning(`Approval Premium Description needed!`)
// if (page === 'kyc' && formData.destinationGroup === "Agent" && !formData.approval_description_premium) return toast.warning(`Approval Agent Description needed!`)
if (!formData.msisdn||!formData.email||!formData.fullname||!formData.username||!formData.mother_fullname||!formData.address||!formData.nationality||!formData.date_birth||!formData.gender) {
return toast.warning(`Required fields cannot be empty: msisdn, email, fullname, username, mother Full Name, Address, Nationality, Date Of Birth, Gender.!`)
}

View File

@ -261,6 +261,8 @@ const DetailApprovalTransaction = () => {
kind = 'TOP UP';
} else if (transactionDetails?.kind === 'R') {
kind = 'RETURN';
}else if (transactionDetails?.kind === 'E') {
kind = 'REWARD';
}
return kind;
})()}
@ -445,6 +447,8 @@ const DetailApprovalTransaction = () => {
kind = 'TOP UP';
} else if (transactionDetails?.kind === 'R') {
kind = 'RETURN';
}else if (transactionDetails?.kind === 'E') {
kind = 'REWARD';
}
return kind;
})()}

View File

@ -281,6 +281,8 @@ const DetailTransaction = () => {
kind = 'TOP UP';
} else if (transactionDetails?.kind === 'R') {
kind = 'RETURN';
}else if (transactionDetails?.kind === 'E') {
kind = 'REWARD';
}
return kind;
})()}
@ -336,7 +338,7 @@ const DetailTransaction = () => {
</div>
</div>
)}
{/* {activeTab === 'detail' && transactionDetails?.kind === 'P' && ( */}
{activeTab === 'detail' && (
<div className="space-y-4">
<h3 className="font-semibold flex items-center">
Product Information
@ -389,7 +391,7 @@ const DetailTransaction = () => {
</div>
</div>
</div>
{/* )} */}
)}
{activeTab === 'detail' && transactionDetails?.kind != 'P' && transactionDetails?.transfer != null && (
<div className="space-y-4">
@ -461,6 +463,8 @@ const DetailTransaction = () => {
kind = 'TOP UP';
} else if (transactionDetails?.kind === 'R') {
kind = 'RETURN';
}else if (transactionDetails?.kind === 'E') {
kind = 'REWARD';
}
return kind;
})()}
@ -492,11 +496,11 @@ const DetailTransaction = () => {
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-gray-500">Full Name</p>
<p className="font-medium">{transactionDetails?.origin_customer?.fullname ?? transactionDetails?.origin_customer?.origin_name}</p>
<p className="font-medium">{transactionDetails?.origin_customer?.origin_name}</p>
</div>
<div>
<p className="text-sm text-gray-500">Phone Number</p>
<p className="font-medium">{transactionDetails?.origin_customer?.msisdn ?? transactionDetails?.origin_msisdn}</p>
<p className="font-medium">{transactionDetails?.origin_msisdn}</p>
</div>
<div>
<p className="text-sm text-gray-500">Email</p>

View File

@ -177,6 +177,7 @@ const ListToolbar = () => {
<SelectItem value="U">TOP UP</SelectItem>
<SelectItem value="R">RETURN</SelectItem>
<SelectItem value="N">TOP UP PARTNER</SelectItem>
<SelectItem value="E">REWARD</SelectItem>
</SelectContent>
</Select>

View File

@ -77,6 +77,7 @@ const TransactionProvider = ({ children }: { children: React.ReactNode }) => {
case 'U': return 'TOP UP';
case 'R': return 'RETURN';
case 'N': return 'TOP UP PARTNER';
case 'E': return 'REWARD';
default: return '_';
}
},

View File

@ -559,6 +559,7 @@ const AddDialog = () => {
<SelectItem value="W">Withdraw</SelectItem>
<SelectItem value="U">Top Up</SelectItem>
<SelectItem value="N">Top Up Patner</SelectItem>
<SelectItem value="E">Reward</SelectItem>
</SelectContent>
</Select>
{errors.status_kind && (

View File

@ -757,6 +757,7 @@ const EditDialog = () => {
<SelectItem value="W">Withdraw</SelectItem>
<SelectItem value="U">Top Up</SelectItem>
<SelectItem value="N">Top Up Patner</SelectItem>
<SelectItem value="E">Reward</SelectItem>
</SelectContent>
</Select>
{errors.status_kind && (

View File

@ -43,7 +43,8 @@ const typeLabelMap: Record<string, string> = {
DM: 'Disbursment Master Agent',
DA: 'Disbursment Agent',
WI: 'Withdraw Merchant',
IC: 'Income Merchant'
IC: 'Income Merchant',
DN: 'Donation'
};
const ListToolbar = () => {

View File

@ -276,7 +276,8 @@ const ManageTransferTypeContextProvider = ({ children }: { children: React.React
P: { label: 'Purchase', className: 'bg-green-100 text-green-600' },
W: { label: 'Withdraw', className: 'bg-fuchsia-100 text-fuchsia-600' },
U: { label: 'Top Up', className: 'bg-yellow-100 text-yellow-600' },
N: { label: 'Top Up Patner', className: 'bg-purple-100 text-purple-600' }
N: { label: 'Top Up Patner', className: 'bg-purple-100 text-purple-600' },
E: {label: 'Reward', className: 'bg-rose-100 text-rose-600'}
};
const kindInfo = mapping[kind] || {

View File

@ -19,6 +19,11 @@ interface WalletProps {
name: string;
}
interface GroupProps {
ID: string;
name: string;
}
const getOneMonthsAgo = () => {
const today = new Date();
return new Date(today.getFullYear(), today.getMonth() - 1, today.getDate());
@ -37,7 +42,11 @@ const ListToolbar = () => {
const [walletId, setWalletId] = useState<string>(
(table.getColumn('id_wallet')?.getFilterValue() as string) ?? ''
);
const [groupId, setGroupId] = useState<string>(
(table.getColumn('id_group')?.getFilterValue() as string) ?? ''
);
const [wallets, setWallets] = useState<WalletProps[]>([]);
const [groups, setGroups] = useState<GroupProps[]>([]);
useEffect(() => {
const today = new Date();
@ -67,6 +76,11 @@ const ListToolbar = () => {
table.setPageIndex(0);
}, [walletId, table]);
useEffect(() => {
table.getColumn('id_group')?.setFilterValue(groupId);
table.setPageIndex(0);
}, [groupId, table]);
useEffect(() => {
if (dateRange.from && dateRange.to) {
handleFilterByDate();
@ -88,8 +102,24 @@ const ListToolbar = () => {
}
};
const fetchGroups = async () => {
try {
const response = await GetData(`${API_URL_WALLET}/dashboard/group/`, {
limit: 100,
page: 1,
with_deleted: false,
order_field: 'created_at',
order_direction: 'ASC'
});
setGroups(response?.data.list || []);
} catch (error) {
console.error('Error fetching groups', error);
}
};
useEffect(() => {
fetchWallets();
fetchGroups();
}, []);
const handleClearAllFilters = () => {
@ -102,10 +132,12 @@ const ListToolbar = () => {
setSearchValue('');
setWalletId('');
setGroupId('');
setDateRange(resetDateRange);
table.getColumn('msisdn')?.setFilterValue('');
table.getColumn('id_wallet')?.setFilterValue('');
table.getColumn('id_group')?.setFilterValue('');
table.getColumn('CreatedAt')?.setFilterValue(resetDateRange);
setTimeout(() => {
@ -114,7 +146,6 @@ const ListToolbar = () => {
}, 0);
};
const handleRefresh = () => {
const today = new Date();
const threeMonthsAgo = getOneMonthsAgo();
@ -125,6 +156,7 @@ const ListToolbar = () => {
setSearchValue('');
setWalletId('');
setGroupId('');
setDateRange(resetDateRange);
table.setColumnFilters([{ id: 'CreatedAt', value: resetDateRange }]);
@ -180,6 +212,22 @@ const ListToolbar = () => {
</SelectContent>
</Select>
</div>
<div className="w-[160px]">
<Select value={groupId} onValueChange={(value) => setGroupId(value)}>
<SelectTrigger className="h-[32px]">
<SelectValue placeholder="Select Group" />
</SelectTrigger>
<SelectContent>
{groups.map((group) => (
<SelectItem key={group.ID} value={group.ID}>
{group.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<DefaultTooltip title={'Reset Filter'} placement={'top'}>
<Button
variant="outline"

View File

@ -6,11 +6,8 @@ import { ColumnDef } from '@tanstack/react-table';
import React, { createContext, useCallback, useMemo, useState } from 'react';
import ListToolbar from '../blocks/ListToolbar';
// Helper function for number formatting with currency format
const formatNumber = (num: number, currencyCode: string = 'USD'): string => {
const formatNumber = (num: number): string => {
return num.toLocaleString('en-US', {
style: 'currency',
currency: currencyCode,
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
@ -79,33 +76,30 @@ const ManageWalletContextProvider = ({ children }: { children: React.ReactNode }
const columns = useMemo<ColumnDef<any>[]>(
() => [
{
accessorKey: 'id_wallet',
header: ({ column }) => <DataGridColumnHeader title="Wallet Name" column={column} />,
cell: ({ row }) => row.original.name || 'Unknown Wallet',
enableSorting: false,
enableHiding: false,
meta: {
headerClassName: 'w-[200px]'
}
},
{
accessorKey: 'msisdn',
header: ({ column }) => <DataGridColumnHeader title="MSISDN" column={column} />,
enableSorting: true,
enableSorting: false,
enableHiding: false,
meta: {
headerClassName: 'w-[550px]',
cellClassName: 'p-[20px]'
}
},
{
accessorKey: 'id_wallet',
header: ({ column }) => <DataGridColumnHeader title="Wallet ID" column={column} />,
enableSorting: false,
enableHiding: true, // Hide this column from view but use it for filtering
meta: {
headerClassName: 'w-[200px]'
}
},
{
accessorKey: 'amount',
header: ({ column }) => <DataGridColumnHeader title="Amount" column={column} />,
cell: ({ row }) => {
// Get currency code from the nested data structure if available
const currencyCode = row.original.balance_type?.currency?.code || 'USD';
return formatNumber(row.original.amount, currencyCode);
},
cell: ({ row }) => formatNumber(row.original.amount),
enableSorting: false,
enableHiding: false,
meta: {
@ -126,11 +120,7 @@ const ManageWalletContextProvider = ({ children }: { children: React.ReactNode }
{
accessorKey: 'amount_this_month',
header: ({ column }) => <DataGridColumnHeader title="Amount This Month" column={column} />,
cell: ({ row }) => {
// Get currency code from the nested data structure if available
const currencyCode = row.original.balance_type?.currency?.code || 'USD';
return formatNumber(row.original.amount_this_month, currencyCode);
},
cell: ({ row }) => formatNumber(row.original.amount_this_month),
enableSorting: false,
enableHiding: false,
meta: {
@ -154,19 +144,18 @@ const ManageWalletContextProvider = ({ children }: { children: React.ReactNode }
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]'
// }
// },
{
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]'
}
},
{
accessorFn: (row) => row.balance_type.currency.name,
id: 'currency_name',
accessorKey: 'currency_name',
header: ({ column }) => <DataGridColumnHeader title="Currency Name" column={column} />,
enableSorting: false,
enableHiding: false,
@ -175,8 +164,7 @@ const ManageWalletContextProvider = ({ children }: { children: React.ReactNode }
}
},
{
accessorFn: (row) => row.balance_type.currency.code,
id: 'currency_code',
accessorKey: 'currency_code',
header: ({ column }) => <DataGridColumnHeader title="Currency Code" column={column} />,
enableSorting: false,
enableHiding: false,
@ -185,9 +173,9 @@ const ManageWalletContextProvider = ({ children }: { children: React.ReactNode }
}
},
{
accessorFn: (row) => row.group.name,
id: 'group_name',
accessorKey: 'id_group',
header: ({ column }) => <DataGridColumnHeader title="Group Name" column={column} />,
cell: ({ row }) => row.original.group_name || 'Unknown Group',
enableSorting: false,
enableHiding: false,
meta: {
@ -195,7 +183,7 @@ const ManageWalletContextProvider = ({ children }: { children: React.ReactNode }
}
},
{
accessorFn: (row) => row.group.is_bank,
accessorFn: (row) => row.group?.is_bank,
id: 'is_bank',
header: ({ column }) => <DataGridColumnHeader title="Is Bank" column={column} />,
enableSorting: false,
@ -213,18 +201,14 @@ const ManageWalletContextProvider = ({ children }: { children: React.ReactNode }
const sortField = sorting.length > 0 ? sorting[0].id : 'created_at';
const sortDirection = sorting.length > 0 ? (sorting[0].desc ? 'ASC' : 'DESC') : 'DESC';
// Initialize filter object
let filterParams: any = {};
// Process filter array
if (Array.isArray(filter)) {
filter.forEach((f: any) => {
// Handle MSISDN search
if (f.id === 'msisdn' && f.value) {
filterParams.msisdn = { like: `%${f.value.toLowerCase()}%` };
}
// Handle date range filter
if (f.id === 'CreatedAt' && f.value?.from && f.value?.to) {
filterParams.created_at = {
from: `${f.value.from} 00:00:00`,
@ -232,10 +216,13 @@ const ManageWalletContextProvider = ({ children }: { children: React.ReactNode }
};
}
// Handle wallet ID filter
if (f.id === 'id_wallet' && f.value) {
filterParams.id_wallet = f.value;
}
if (f.id === 'id_group' && f.value) {
filterParams.id_group = f.value;
}
});
}
@ -253,8 +240,81 @@ const ManageWalletContextProvider = ({ children }: { children: React.ReactNode }
return { data: [], totalCount: 0 };
}
setWallets(response?.data.list);
return { data: response?.data.list, totalCount: response?.data.total_count };
const walletsResponse = await GetData(`${API_URL_WALLET}/dashboard/wallet/`, {
limit: 100,
page: 1,
with_deleted: false,
order_field: 'created_at',
order_direction: 'ASC'
});
const groupsResponse = await GetData(`${API_URL_WALLET}/dashboard/group/`, {
limit: 100,
page: 1,
with_deleted: false,
order_field: 'created_at',
order_direction: 'ASC'
});
const currenciesResponse = await GetData(`${API_URL_WALLET}/dashboard/currency/`, {
limit: 100,
page: 1,
with_deleted: false,
order_field: 'created_at',
order_direction: 'ASC'
});
const walletsMap = walletsResponse?.data?.list
? walletsResponse.data.list.reduce((acc: any, wallet: any) => {
acc[wallet.ID] = {
name: wallet.name,
id_currency: wallet.id_currency
};
return acc;
}, {})
: {};
const groupsMap = groupsResponse?.data?.list
? groupsResponse.data.list.reduce((acc: any, group: any) => {
acc[group.ID] = group.name;
return acc;
}, {})
: {};
const currenciesMap = currenciesResponse?.data?.list
? currenciesResponse.data.list.reduce((acc: any, currency: any) => {
acc[currency.ID] = {
name: currency.name,
code: currency.code
};
return acc;
}, {})
: {};
const enrichedData = response?.data.list.map((item: any) => {
const walletInfo = walletsMap[item.id_wallet] || {
name: 'Unknown Wallet',
id_currency: null
};
const currencyInfo = currenciesMap[walletInfo.id_currency] || {
name: 'Unknown Currency',
code: 'USD'
};
return {
...item,
name: walletInfo.name,
group_name: groupsMap[item.id_group] || 'Unknown Group',
currency_name: currencyInfo.name,
currency_code: currencyInfo.code
};
});
setWallets(enrichedData);
return {
data: enrichedData,
totalCount: response?.data.total_count
};
} catch (error) {
console.error('Error fetching Wallet', error);
return { data: [], totalCount: 0 };

View File

@ -45,6 +45,7 @@ import WalletHistory from '@/pages/wallet/wallet-history/WalletHistory';
import WalletMaster from '@/pages/master/wallet/WalletMaster';
import CurrencyMaster from '@/pages/master/currency/CurrencyMaster';
import FeedbackMemberMaster from '@/pages/members/feedback-member/FeedbackMember';
import AgentBalance from '@/pages/members/agent-balance/AgentBalance';
// DISBURSEMENT
import HistoryTransactionDisbursement from '@/pages/disbursement/history-transaction/HistoryTransaction';
@ -91,6 +92,7 @@ const AppRoutingSetup = (): ReactElement => {
<Route path="/members/kyc-delete-member" element={<ManageKycDeletion />} />
<Route path="/members/create-member-credential" element={<MemberCredential />} />
<Route path="/members/feedback-member" element={<FeedbackMemberMaster />} />
<Route path="/members/agent-balance" element={<AgentBalance />} />
<Route path="/access/access-type-management" element={<AccessType />} />