This commit is contained in:
Raja Oktafrianto
2025-05-21 00:08:44 +07:00
32 changed files with 1646 additions and 691 deletions

Binary file not shown.

View File

@ -9,6 +9,7 @@ export interface AuthModel {
company: any; company: any;
user: any; user: any;
// api_token: string; // api_token: string;
statusbalance: string;
} }
export interface UserModel { export interface UserModel {

View File

@ -43,24 +43,6 @@ const AuthProvider = ({ children }: PropsWithChildren) => {
const [auth, setAuth] = useState<AuthModel | undefined>(authHelper.getAuth()); const [auth, setAuth] = useState<AuthModel | undefined>(authHelper.getAuth());
const [currentUser, setCurrentUser] = useState<UserModel | undefined>(); const [currentUser, setCurrentUser] = useState<UserModel | undefined>();
const verify = async () => {
if (auth) {
try {
const { data: user } = await getUser();
const createCacheUser = {
name: user.name,
email: user.email,
username: user.username,
role_name: auth.role_name
};
localStorage.setItem('user', JSON.stringify(createCacheUser));
} catch {
saveAuth(undefined);
setCurrentUser(undefined);
}
}
};
const saveAuth = (auth: AuthModel | undefined) => { const saveAuth = (auth: AuthModel | undefined) => {
setAuth(auth); setAuth(auth);
if (auth) { if (auth) {
@ -73,11 +55,20 @@ const AuthProvider = ({ children }: PropsWithChildren) => {
const login = async (username: string, password: string) => { const login = async (username: string, password: string) => {
try { try {
const { data: auth } = await axios const { data: auth } = await axios
.post(LOGIN_URL, { username, password }) // , { headers: { 'Access-Control-Allow-Origin': "*" }} .post(LOGIN_URL, { username, password })
.then((response) => response.data); .then((response) => response.data);
saveAuth({ ...auth.token, id: auth.user.id, role_name: auth.role_name, user: auth.user }); const enhancedAuth: AuthModel = {
...auth.token,
id: auth.user.id,
role_name: auth.role_name,
user: auth.user,
statusbalance: auth.role?.status_balance ?? null // SAFE ACCESS
};
saveAuth(enhancedAuth);
setCurrentUser(auth.user); setCurrentUser(auth.user);
const createActivity = { const createActivity = {
module: 'Login', module: 'Login',
description: `Login`, description: `Login`,
@ -85,6 +76,7 @@ const AuthProvider = ({ children }: PropsWithChildren) => {
}; };
doSaveLogActivity(createActivity); doSaveLogActivity(createActivity);
} catch (error: any) { } catch (error: any) {
console.error('Login error:', error);
throw error; throw error;
} }
}; };
@ -108,6 +100,33 @@ const AuthProvider = ({ children }: PropsWithChildren) => {
return { data: _axios }; return { data: _axios };
}; };
const verify = async () => {
if (auth) {
try {
const { data: user } = await getUser();
// Perbarui auth yang sekarang dengan statusbalance
saveAuth({
...auth,
statusbalance: user.role.status_balance
});
const createCacheUser = {
name: user.name,
email: user.email,
username: user.username,
role_name: auth.role_name,
statusbalance: user.role.status_balance
};
localStorage.setItem('user', JSON.stringify(createCacheUser));
} catch {
saveAuth(undefined);
setCurrentUser(undefined);
}
}
};
const logout = async () => { const logout = async () => {
const createActivity = { const createActivity = {
module: 'Logout', module: 'Logout',
@ -130,7 +149,6 @@ const AuthProvider = ({ children }: PropsWithChildren) => {
currentUser, currentUser,
setCurrentUser, setCurrentUser,
login, login,
// register,
requestPasswordResetLink, requestPasswordResetLink,
changePassword, changePassword,
getUser, getUser,
@ -143,4 +161,4 @@ const AuthProvider = ({ children }: PropsWithChildren) => {
); );
}; };
export { AuthContext, AuthProvider }; export { AuthContext, AuthProvider };

View File

@ -23,6 +23,7 @@ import { apiConfig } from '@/config/api.config';
import TransactionValue from './blocks/TransactionValue'; import TransactionValue from './blocks/TransactionValue';
import TransactionPieChart from './blocks/TransactionPieChart'; import TransactionPieChart from './blocks/TransactionPieChart';
import MemberActivity from './blocks/MemberActivity'; import MemberActivity from './blocks/MemberActivity';
import BankSaldo from './blocks/BankSaldo';
// sum -> nominal, count-> total // sum -> nominal, count-> total
type CountType = 'sum' | 'count'; type CountType = 'sum' | 'count';
@ -57,6 +58,8 @@ const DashboardHomePage = () => {
const API_URL = apiConfig.api_dashboard; const API_URL = apiConfig.api_dashboard;
const { GetData } = useCallApi(); const { GetData } = useCallApi();
const API_URL_BANK = apiConfig.service_wallet;
const [responseStatisticCard, setResponseStatisticCard] = useState<any>(null); const [responseStatisticCard, setResponseStatisticCard] = useState<any>(null);
const fetchData = async () => { const fetchData = async () => {
@ -64,10 +67,21 @@ const DashboardHomePage = () => {
setResponseStatisticCard(res); setResponseStatisticCard(res);
}; };
const [bankaccount, setbankaccount] = useState<any>(null);
const fetchDataBankAccount = async () => {
const res = await GetData(`${API_URL_BANK}/dashboard/balance/account/${getAuth()?.user?.customer?.id}`, {});
setbankaccount(res);
};
useEffect(() => { useEffect(() => {
fetchData(); fetchData();
}, []); }, []);
useEffect(() => {
fetchDataBankAccount();
}, []);
const [responseGraphic, setresponseGraphic] = useState<any>(null); const [responseGraphic, setresponseGraphic] = useState<any>(null);
useEffect(() => { useEffect(() => {
@ -253,8 +267,24 @@ const DashboardHomePage = () => {
</div> </div>
) : null} ) : null}
<div className="flex space-x-4 mt-5">
{bankaccount?.data && bankaccount?.data.length > 0 && getAuth()?.statusbalance=='Y' ? (
bankaccount?.data.map((bankaccountdatas: { amount: string, creditlimit: string, monthlylimit: string; wallet: string; }, index: number) => (
<BankSaldo
title={bankaccountdatas.wallet}
balance={bankaccountdatas.amount}
creditLimit={bankaccountdatas.creditlimit}
monthlyLimit={bankaccountdatas.monthlylimit}
/>
))
) : (
""
)}
</div>
{/* Cards */} {/* Cards */}
<div className="flex gap-6 overflow-x-auto pb-2"> <div className="flex gap-6 overflow-x-auto pb-2 mt-5">
<Card <Card
title="Registered Users" title="Registered Users"
total={responseStatisticCard?.data.total_registered ?? 0} total={responseStatisticCard?.data.total_registered ?? 0}

View File

@ -0,0 +1,62 @@
// BankSaldo.tsx
import { Wallet } from "lucide-react";
interface AccountCardProps {
title: string;
balance: string;
creditLimit: string;
monthlyLimit: string;
}
export const BankSaldo = ({
title,
balance,
creditLimit,
monthlyLimit,
}: AccountCardProps) => {
return (
<div className="w-full max-w-xs bg-white rounded-xl shadow-md overflow-hidden border border-gray-200">
<div className="h-1 bg-red-500" />
<div className="p-4 space-y-4">
<div className="flex justify-between items-center">
<h2 className="text-lg font-semibold">{title}</h2>
<Wallet className="h-5 w-5 text-gray-500" />
</div>
<div className="text-2xl font-bold text-gray-800">
{new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(parseFloat(balance))}
</div>
<div className="text-sm text-gray-600">
<div className="flex justify-between">
<span>Credit Limit:</span>
<span>{creditLimit}</span>
</div>
<div className="flex justify-between">
<span>Monthly Limit:</span>
<span>{monthlyLimit}</span>
</div>
</div>
</div>
</div>
);
};
interface AccountCardsProps {
accounts: AccountCardProps[];
}
export const AccountCards = ({ accounts }: AccountCardsProps) => {
return (
<div className="flex flex-wrap gap-4 justify-center">
{accounts.map((account, index) => (
<BankSaldo key={index} {...account} />
))}
</div>
);
};
export default BankSaldo

View File

@ -10,12 +10,37 @@ interface Props {
enddate: string; enddate: string;
} }
const polarToCartesian = (cx: number, cy: number, radius: number, angleInDegrees: number) => {
const angleInRadians = (angleInDegrees * Math.PI) / 180.0;
return {
x: cx + radius * Math.cos(angleInRadians),
y: cy + radius * Math.sin(angleInRadians),
};
};
const describeArc = (
x: number,
y: number,
radius: number,
startAngle: number,
endAngle: number
) => {
const start = polarToCartesian(x, y, radius, endAngle);
const end = polarToCartesian(x, y, radius, startAngle);
const largeArcFlag = endAngle - startAngle <= 180 ? "0" : "1";
return [
"M", start.x, start.y,
"A", radius, radius, 0, largeArcFlag, 0, end.x, end.y,
].join(" ");
};
const MemberActivity = ({ startdate, enddate }: Props) => { const MemberActivity = ({ startdate, enddate }: Props) => {
const { GetData } = useCallApi(); const { GetData } = useCallApi();
const [responseTransactionValue, setResponseTransactionValue] = useState<any>(null); const [responseTransactionValue, setResponseTransactionValue] = useState<any>(null);
useEffect(() => { useEffect(() => {
const fetchDataTransactionValue = async () => { const fetchData = async () => {
try { try {
const res = await GetData(`${API_URL}/active-user`, { const res = await GetData(`${API_URL}/active-user`, {
date_from: startdate, date_from: startdate,
@ -23,43 +48,33 @@ const MemberActivity = ({ startdate, enddate }: Props) => {
}); });
setResponseTransactionValue(res); setResponseTransactionValue(res);
} catch (error) { } catch (error) {
console.error('Error fetching transaction value:', error); console.error('Error fetching data:', error);
} }
}; };
fetchDataTransactionValue(); fetchData();
}, [startdate, enddate, GetData]); }, [startdate, enddate, GetData]);
const percentage = parseFloat((responseTransactionValue?.data?.total_customer_active_percentage ?? 0).toFixed(2)) ?? 0; const percentage = parseFloat((responseTransactionValue?.data?.total_customer_active_percentage ?? 0).toFixed(2));
const totalCustomer = responseTransactionValue?.data?.total_customer ?? 0; const totalCustomer = responseTransactionValue?.data?.total_customer ?? 0;
const activeCustomer = responseTransactionValue?.data?.total_customer_active ?? 0; const activeCustomer = responseTransactionValue?.data?.total_customer_active ?? 0;
const reguler = responseTransactionValue?.data?.reguler ?? 0; const reguler = responseTransactionValue?.data?.reguler ?? 0;
const premium = responseTransactionValue?.data?.premium ?? 0; const premium = responseTransactionValue?.data?.premium ?? 0;
const agent = responseTransactionValue?.data?.agent ?? 0; const agent = responseTransactionValue?.data?.agent ?? 0;
// Hitung sudut pointer // Calculate pointer angle
const angle = (percentage / 100) * 180; // 0° (kiri) ke 180° (kanan) const angle = (percentage / 100) * 180;
const radians = (angle * Math.PI) / 180; const pointerX = 50 + 25 * Math.cos((angle - 180) * Math.PI / 180);
const radius = 40; // Radius dari setengah lingkaran const pointerY = 50 + 25 * Math.sin((angle - 180) * Math.PI / 180);
const center = 50; // Titik pusat lingkaran
const pointerLength = 25; // Panjang pointer
const x = center + pointerLength * Math.cos(radians - Math.PI); // offset agar mulai dari kiri
const y = center + pointerLength * Math.sin(radians - Math.PI);
// Hitung titik akhir untuk arc aktif
const arcAngle = (Math.PI * percentage) / 100;
const arcX = 50 + radius * Math.cos(Math.PI - arcAngle);
const arcY = 50 - radius * Math.sin(arcAngle);
return ( return (
<div className="p-6 bg-white rounded-lg shadow-md w-1/3"> <div className="p-6 bg-white rounded-lg shadow-md w-full max-w-md">
<div className="flex justify-between items-center pb-3 mb-4"> <div className="pb-3 mb-4">
<h2 className="text-lg font-semibold text-gray-700">Member Activity</h2> <h2 className="text-lg font-semibold text-gray-700">Member Activity</h2>
</div> </div>
<div className="flex gap-6"> <div className="flex gap-6">
{/* Sidebar */} {/* Sidebar Info */}
<ul className="space-y-4 w-1/2 text-gray-700 text-sm"> <ul className="space-y-4 w-1/2 text-gray-700 text-sm">
<li className="flex items-center gap-2"> <li className="flex items-center gap-2">
<UsersIcon className="w-4 h-4" /> <UsersIcon className="w-4 h-4" />
@ -70,15 +85,15 @@ const MemberActivity = ({ startdate, enddate }: Props) => {
<span>Active Customer: {activeCustomer}</span> <span>Active Customer: {activeCustomer}</span>
</li> </li>
<li className="flex items-center gap-2"> <li className="flex items-center gap-2">
<UserCheckIcon className="w-4 h-4" color='blue'/> <UserCheckIcon className="w-4 h-4" color='blue' />
<span>Reguler: {reguler}</span> <span>Reguler: {reguler}</span>
</li> </li>
<li className="flex items-center gap-2"> <li className="flex items-center gap-2">
<UserCheckIcon className="w-4 h-4" color='green'/> <UserCheckIcon className="w-4 h-4" color='green' />
<span>Premium: {premium}</span> <span>Premium: {premium}</span>
</li> </li>
<li className="flex items-center gap-2"> <li className="flex items-center gap-2">
<UserCheckIcon className="w-4 h-4" color='orange'/> <UserCheckIcon className="w-4 h-4" color='orange' />
<span>Agent: {agent}</span> <span>Agent: {agent}</span>
</li> </li>
</ul> </ul>
@ -88,7 +103,7 @@ const MemberActivity = ({ startdate, enddate }: Props) => {
<div className="border border-gray-200 rounded-md p-4 text-center"> <div className="border border-gray-200 rounded-md p-4 text-center">
<h3 className="text-sm text-gray-600 font-medium mb-2">Active User</h3> <h3 className="text-sm text-gray-600 font-medium mb-2">Active User</h3>
<div className="relative h-24 w-full"> <div className="relative h-24 w-full">
<svg className="w-full h-full" viewBox="0 0 100 50"> <svg viewBox="0 0 100 50" className="w-full h-full">
{/* Background arc */} {/* Background arc */}
<path <path
d="M 10 50 A 40 40 0 0 1 90 50" d="M 10 50 A 40 40 0 0 1 90 50"
@ -97,24 +112,27 @@ const MemberActivity = ({ startdate, enddate }: Props) => {
strokeWidth="10" strokeWidth="10"
/> />
{/* Active arc */} {/* Active arc */}
<path {percentage > 0 && (
d={`M 10 50 A 40 40 0 ${percentage > 50 ? 1 : 0} 1 ${arcX} ${arcY}`} <path
fill="none" d={describeArc(50, 50, 40, 180, 180 + (percentage * 180 / 100))}
stroke="#34d399" fill="none"
strokeWidth="10" stroke="#34d399"
/> strokeWidth="10"
strokeLinecap="round"
/>
)}
{/* Pointer */} {/* Pointer */}
<line <line
x1="50" x1="50"
y1="50" y1="50"
x2={x} x2={pointerX}
y2={y} y2={pointerY}
stroke="#111827" stroke="#111827"
strokeWidth="4" strokeWidth="4"
strokeLinecap="round" strokeLinecap="round"
/> />
</svg> </svg>
<div className="flex justify-between text-xs text-gray-500 px-1"> <div className="flex justify-between text-xs text-gray-500 px-1 mt-1">
<span>{percentage}%</span> <span>{percentage}%</span>
<span>100%</span> <span>100%</span>
</div> </div>

View File

@ -43,7 +43,11 @@ const TransactionPieChart = ({ startdate, enddate }: Props) => {
{ name: 'Purchase', value: parseFloat((responseTransactionValue?.data?.P ?? 0).toFixed(2)), color: '#baf7c5' }, { 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: '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: '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' }, { name: 'Reward', value: parseFloat((responseTransactionValue?.data?.E ?? 0).toFixed(2)), color: '#f50a19' },
{ name: 'Purchase Loja', value: parseFloat((responseTransactionValue?.data?.L ?? 0).toFixed(2)), color: '#F0A04B' },
{ name: 'Top Up P24', value: parseFloat((responseTransactionValue?.data?.B ?? 0).toFixed(2)), color: '#FADA7A' },
{ name: 'Transfer Agent', value: parseFloat((responseTransactionValue?.data?.A ?? 0).toFixed(2)), color: '#B1C29E' },
{ name: 'Withdrawal Agent', value: parseFloat((responseTransactionValue?.data?.M ?? 0).toFixed(2)), color: '#FCE7C8' },
]; ];
return ( return (

View File

@ -67,6 +67,18 @@ const TransactionValue = ({ startdate, enddate }: Props) => {
case "E": case "E":
type = "Reward"; type = "Reward";
break; break;
case "L":
type = "Purchase Loja"
break;
case "B":
type ="Top Up P24";
break;
case "A":
type = " Transfer Agent";
break;
case "M":
type = "Withdrawal Agent";
break;
default: default:
type = "Unknown"; type = "Unknown";
break; break;

View File

@ -5,15 +5,15 @@ import { useCallApi } from '@/hooks';
import { apiConfig } from '@/config/api.config'; import { apiConfig } from '@/config/api.config';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import moment from 'moment'; import moment from 'moment';
import { getAuth } from '@/auth';
import { toast } from 'sonner';
import { import {
Dialog, Dialog,
DialogBody, DialogBody,
DialogContent, DialogContent,
DialogDescription,
DialogHeader, DialogHeader,
DialogTitle DialogTitle
} from '@/components/ui/dialog'; } from '@/components/ui/dialog';
import TransactionLogViewer from './DetailTransactionLog';
const API_URL = apiConfig.service_disbursement; const API_URL = apiConfig.service_disbursement;
@ -57,6 +57,7 @@ const DetailTransaction = () => {
const [transactionDetails, setTransactionDetails] = useState<any>(null); const [transactionDetails, setTransactionDetails] = useState<any>(null);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isExporting, setIsExporting] = useState(false);
useEffect(() => { useEffect(() => {
const fetchTransactionDetails = async () => { const fetchTransactionDetails = async () => {
@ -69,7 +70,6 @@ const DetailTransaction = () => {
id: selectedTransactionId id: selectedTransactionId
} }
); );
// console.log(response?.data);
setTransactionDetails(response?.data); setTransactionDetails(response?.data);
} catch (error) { } catch (error) {
console.error('Error fetching transaction', error); console.error('Error fetching transaction', error);
@ -84,8 +84,6 @@ const DetailTransaction = () => {
} }
}, [showDetailDialog, selectedTransactionId, GetData]); }, [showDetailDialog, selectedTransactionId, GetData]);
const [activeTab, setActiveTab] = useState('detail'); // 'detail', 'log', 'approve'
const resetForm = () => { const resetForm = () => {
setTransactionDetails(null); setTransactionDetails(null);
}; };
@ -96,109 +94,230 @@ const DetailTransaction = () => {
} }
}, [showDetailDialog]); }, [showDetailDialog]);
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}`
}
}
);
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]);
}
}
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 ( return (
<Dialog open={showDetailDialog} onOpenChange={setShowDetailDialog}> <Dialog open={showDetailDialog} onOpenChange={setShowDetailDialog}>
<DialogContent className="container-fixed max-w-[1024px] flex flex-col p-5 overflow-hidden"> <DialogContent className="container-fixed max-w-[1280px] w-full h-[90vh] flex flex-col p-6 overflow-hidden">
<DialogHeader> <DialogHeader>
<DialogTitle>Transaction Details</DialogTitle> <DialogTitle className="text-2xl font-bold">Transaction Details</DialogTitle>
</DialogHeader> </DialogHeader>
<DialogBody> <DialogBody>
{/* Tab Content */} <div className="py-4 overflow-y-auto max-h-[600px] space-y-6">
<div className="py-4 overflow-y-auto max-h-[400px]"> {/* Summary Info */}
<div className="space-y-4"> {transactionDetails && (
<div className="border rounded-lg overflow-x-auto"> <div className="grid grid-cols-2 gap-y-4 gap-x-6 border rounded-lg p-4 bg-gray-50">
{isLoading ? ( <div>
<div className="flex flex-col items-center justify-center p-8"> <p className="text-sm text-gray-500">Filename Upload</p>
<div className="animate-pulse flex space-x-4 w-full"> <p className="text-base text-gray-800">{transactionDetails.file_name ?? '-'}</p>
<div className="flex-1 space-y-4 py-1"> </div>
<div className="h-4 bg-gray-200 rounded w-3/4"></div> <div>
<div className="space-y-2"> <p className="text-sm text-gray-500">Total Amount</p>
<div className="h-4 bg-gray-200 rounded"></div> <p className="text-base text-gray-800">{transactionDetails.amount ?? '-'}</p>
<div className="h-4 bg-gray-200 rounded w-5/6"></div> </div>
</div> <div>
<p className="text-sm text-gray-500">Total Record</p>
<p className="text-base text-gray-800">
{transactionDetails.total_record ?? '-'}
</p>
</div>
<div>
<p className="text-sm text-gray-500">Success Record</p>
<p className="text-base text-gray-800">
{transactionDetails.total_success ?? '-'}
</p>
</div>
<div>
<p className="text-sm text-gray-500">Fail Record</p>
<p className="text-base text-gray-800">{transactionDetails.total_fail ?? '-'}</p>
</div>
<div>
<p className="text-sm text-gray-500">Pending Record</p>
<p className="text-base text-gray-800">
{transactionDetails.total_pending ?? '-'}
</p>
</div>
<div>
<p className="text-sm text-gray-500">Status</p>
<p className="text-base text-gray-800">
{renderStatusBadge(transactionDetails.status)}
</p>
</div>
<div>
<p className="text-sm text-gray-500">Execution Date</p>
<p className="text-base text-gray-800">
{transactionDetails.execution_date
? moment(transactionDetails.execution_date).format('DD/MM/YYYY HH:mm')
: '-'}
</p>
</div>
<div>
<p className="text-sm text-gray-500">Done Date</p>
<p className="text-base text-gray-800">
{transactionDetails.done_date
? moment(transactionDetails.done_date).format('DD/MM/YYYY HH:mm')
: '-'}
</p>
</div>
</div>
)}
{/* Log Table */}
<div className="border rounded-lg overflow-x-auto">
{isLoading ? (
<div className="flex flex-col items-center justify-center p-8">
<div className="animate-pulse flex space-x-4 w-full">
<div className="flex-1 space-y-4 py-1">
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
<div className="space-y-2">
<div className="h-4 bg-gray-200 rounded"></div>
<div className="h-4 bg-gray-200 rounded w-5/6"></div>
</div> </div>
</div> </div>
<p className="mt-4 text-gray-500">Loading Logs Details...</p>
</div> </div>
) : ( <p className="mt-4 text-gray-500">Loading Logs Details...</p>
<table className="min-w-full table-auto"> </div>
<thead> ) : (
<tr className="bg-gray-100"> <table className="min-w-full table-auto">
<th className="px-4 py-2 text-left text-sm text-gray-500">Username</th> <thead>
<th className="px-4 py-2 text-left text-sm text-gray-500">Fullname</th> <tr className="bg-gray-100">
<th className="px-4 py-2 text-left text-sm text-gray-500">Amount</th> <th className="px-4 py-2 text-left text-sm text-gray-500">Username</th>
<th className="px-4 py-2 text-left text-sm text-gray-500">Status</th> <th className="px-4 py-2 text-left text-sm text-gray-500">Fullname</th>
<th className="px-4 py-2 text-left text-sm text-gray-500">Process Date</th> <th className="px-4 py-2 text-left text-sm text-gray-500">Amount</th>
<th className="px-4 py-2 text-left text-sm text-gray-500"> <th className="px-4 py-2 text-left text-sm text-gray-500">Status</th>
Invoice Number <th className="px-4 py-2 text-left text-sm text-gray-500">Process Date</th>
</th> <th className="px-4 py-2 text-left text-sm text-gray-500">Invoice Number</th>
<th className="px-4 py-2 text-left text-sm text-gray-500">Actions</th> <th className="px-4 py-2 text-left text-sm text-gray-500">Remark 1</th>
</tr> <th className="px-4 py-2 text-left text-sm text-gray-500">Remark 2</th>
</thead> <th className="px-4 py-2 text-left text-sm text-gray-500">Remark 3</th>
<tbody> <th className="px-4 py-2 text-left text-sm text-gray-500">Actions</th>
{transactionDetails?.log && transactionDetails.log.length > 0 ? ( </tr>
transactionDetails.log.map( </thead>
( <tbody>
log: { {transactionDetails?.log && transactionDetails.log.length > 0 ? (
id: number; transactionDetails.log.map((log: any, index: number) => (
customer: any; <tr key={index} className="border-t">
amount: number; <td className="px-4 py-2 text-sm text-gray-500">
remark: string; {log.customer?.username ?? 'Not Found'}
reference: string; </td>
request_date: string; <td className="px-4 py-2 text-sm text-gray-500">
payment_response: string; {log.customer?.fullname ?? 'Not Found'}
status: string; </td>
}, <td className="px-4 py-2 text-sm text-gray-500">{log.amount ?? '-'}</td>
index: number <td className="px-4 py-2 text-sm text-gray-500">
) => ( {renderStatusBadge(log.status) ?? '-'}
<tr key={index} className="border-t"> </td>
<td className="px-4 py-2 text-sm text-gray-500"> <td className="px-4 py-2 text-sm text-gray-500">
{log.customer?.username ?? 'Not Found'} {log.request_date && moment(log.request_date).isValid()
</td> ? moment(log.request_date).format('DD/MM/YYYY HH:mm')
<td className="px-4 py-2 text-sm text-gray-500"> : '-'}
{log.customer?.fullname ?? 'Not Found'} </td>
</td> <td className="px-4 py-2 text-sm text-gray-500">
<td className="px-4 py-2 text-sm text-gray-500"> {log.reference ?? '-'}
{log.amount ?? '-'} </td>
</td> <td className="px-4 py-2 text-sm text-gray-500">{log.remark_1 ?? '-'}</td>
<td className="px-4 py-2 text-sm text-gray-500"> <td className="px-4 py-2 text-sm text-gray-500">{log.remark_2 ?? '-'}</td>
{renderStatusBadge(log.status) ?? '-'} <td className="px-4 py-2 text-sm text-gray-500">{log.remark_3 ?? '-'}</td>
</td> <td className="px-4 py-2 text-sm text-gray-500">
<td className="px-4 py-2 text-sm text-gray-500"> <button
{log.request_date && moment(log.request_date).isValid() className="btn btn-sm btn-icon btn-clear btn-light"
? moment(log.request_date).format('DD/MM/YYYY HH:mm:ss') onClick={() => {
: '-'} setDetailLogData(log);
</td> setShowDetailLogDialog(true);
<td className="px-4 py-2 text-sm text-gray-500"> }}
{log.reference ?? '-'} >
</td> <KeenIcon icon="eye" />
<td className="px-4 py-2 text-sm text-gray-500"> </button>
<div key={`actions-${log.id}`}>
<button
className="btn btn-sm btn-icon btn-clear btn-light"
onClick={() => {
setDetailLogData(log);
setShowDetailLogDialog(true);
}}
>
<KeenIcon icon="eye" />
</button>
</div>
</td>
</tr>
)
)
) : (
<tr>
<td colSpan={8} className="px-4 py-2 text-center text-sm text-gray-500">
No logs available
</td> </td>
</tr> </tr>
)} ))
</tbody> ) : (
</table> <tr>
<td colSpan={10} className="px-4 py-2 text-center text-sm text-gray-500">
No logs available
</td>
</tr>
)}
</tbody>
</table>
)}
</div>
{/* Export Button - Moved to bottom right after table */}
<div className="flex justify-end">
<button
className={`px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors flex items-center ${isExporting ? 'opacity-50 pointer-events-none' : ''}`}
onClick={() => {
if (!isExporting) {
handleExport();
}
}}
>
{isExporting ? (
<>
<span className="animate-spin mr-2">
<KeenIcon icon="spinner" />
</span>
Exporting...
</>
) : (
<>
<KeenIcon icon="download" className="mr-2" />
Export
</>
)} )}
</div> </button>
</div> </div>
</div> </div>
</DialogBody> </DialogBody>
@ -207,4 +326,4 @@ const DetailTransaction = () => {
); );
}; };
export default DetailTransaction; export default DetailTransaction;

View File

@ -1,21 +1,13 @@
import React, { useState } from 'react'; import React from 'react';
import moment from 'moment'; import moment from 'moment';
import { useTransactionContext } from '../hooks/useTransactionContext'; import { useTransactionContext } from '../hooks/useTransactionContext';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogBody } from '@/components/ui/dialog'; import {
Dialog,
interface LogType { DialogContent,
id: string; DialogHeader,
amount: number; DialogTitle,
remark: string; DialogBody
reference: string; } from '@/components/ui/dialog';
response_date: string;
status: string;
customer?: {
username: string;
fullname: string;
};
payment_response?: string;
}
type StatusCode = 'W' | 'O' | 'F' | 'D'; type StatusCode = 'W' | 'O' | 'F' | 'D';
@ -29,97 +21,171 @@ const statusMap: Record<StatusCode, StatusInfo> = {
W: { label: 'Waiting Schedule', bg: 'bg-yellow-100', text: 'text-yellow-600' }, W: { label: 'Waiting Schedule', bg: 'bg-yellow-100', text: 'text-yellow-600' },
O: { label: 'On Process', bg: 'bg-blue-100', text: 'text-blue-600' }, O: { label: 'On Process', bg: 'bg-blue-100', text: 'text-blue-600' },
F: { label: 'Fail', bg: 'bg-red-100', text: 'text-red-600' }, F: { label: 'Fail', bg: 'bg-red-100', text: 'text-red-600' },
D: { label: 'Done', bg: 'bg-green-100', text: 'text-green-600' }, D: { label: 'Done', bg: 'bg-green-100', text: 'text-green-600' }
}; };
export const renderStatusBadge = (statusRaw: string | null | undefined) => { export const renderStatusBadge = (statusRaw: string | null | undefined) => {
const status = statusRaw as StatusCode; const status = statusRaw as StatusCode;
const { label, bg, text } = statusMap[status] ?? { const { label, bg, text } = statusMap[status] ?? {
label: 'Unknown', label: 'Unknown',
bg: 'bg-gray-100', bg: 'bg-gray-100',
text: 'text-gray-600', text: 'text-gray-600'
};
return (
<span className={`px-2 py-1 text-xs font-semibold rounded-full ${bg} ${text}`}>
{label}
</span>
);
}; };
return (
<span className={`px-2 py-1 text-xs font-semibold rounded-full ${bg} ${text}`}>{label}</span>
);
};
const TransactionLogViewer = () => { const TransactionLogViewer = () => {
const { const { showDetailLogDialog, setShowDetailLogDialog, detailLogData } = useTransactionContext();
showDetailLogDialog,
setShowDetailLogDialog,
detailLogData
} = useTransactionContext();
return ( return (
<Dialog open={showDetailLogDialog} onOpenChange={setShowDetailLogDialog}> <Dialog open={showDetailLogDialog} onOpenChange={setShowDetailLogDialog}>
<DialogContent className="container-fixed max-w-[1280px] w-full h-[90vh] flex flex-col p-6 overflow-hidden"> <DialogContent className="container-fixed max-w-[1280px] w-full h-[90vh] flex flex-col p-6 overflow-hidden">
<DialogHeader> <DialogHeader>
<DialogTitle>Detail Record</DialogTitle> <DialogTitle>Detail Record</DialogTitle>
</DialogHeader> </DialogHeader>
<DialogBody > <DialogBody className="overflow-y-auto max-h-[70vh] scroll-smooth scrollbar-thin scrollbar-thumb-gray-400 scrollbar-track-gray-100">
{/* Tab Content */} {detailLogData ? (
{detailLogData && detailLogData != null ? ( <div className="py-4 overflow-y-auto">
<div className="py-4 overflow-y-auto"> <div className="space-y-4">
<div className="space-y-4"> <div className="border rounded-lg overflow-x-auto">
<div className="border rounded-lg overflow-x-auto"> <table className="min-w-full table-auto">
<table className='min-w-full table-auto'> <tbody>
<tbody> <tr className="border-t">
<tr className='border-t'> <td className="px-4 py-2 text-sm text-gray-500">Log ID</td>
<td className="px-4 py-2 text-sm text-gray-500">ID</td> <td className="px-4 py-2 text-sm text-gray-800">{detailLogData.id}</td>
<td className="px-4 py-2 text-sm text-gray-500">{detailLogData.customer.id}</td> </tr>
</tr> {detailLogData.customer && (
<tr className='border-t'> <>
<td className="px-4 py-2 text-sm text-gray-500">Username</td> <tr className="border-t">
<td className="px-4 py-2 text-sm text-gray-500">{detailLogData.customer.username}</td> <td className="px-4 py-2 text-sm text-gray-500">Customer ID</td>
</tr> <td className="px-4 py-2 text-sm text-gray-800">
<tr className='border-t'> {detailLogData.customer.id}
<td className="px-4 py-2 text-sm text-gray-500">Name</td> </td>
<td className="px-4 py-2 text-sm text-gray-500">{detailLogData.customer.fullname}</td> </tr>
</tr> <tr className="border-t">
<tr className='border-t'> <td className="px-4 py-2 text-sm text-gray-500">Username</td>
<td className="px-4 py-2 text-sm text-gray-500">Amount</td> <td className="px-4 py-2 text-sm text-gray-800">
<td className="px-4 py-2 text-sm text-gray-500">{detailLogData.amount}</td> {detailLogData.customer.username}
</tr> </td>
<tr className='border-t'> </tr>
<td className="px-4 py-2 text-sm text-gray-500">Remark</td> <tr className="border-t">
<td className="px-4 py-2 text-sm text-gray-500">{detailLogData.remark}</td> <td className="px-4 py-2 text-sm text-gray-500">Fullname</td>
</tr> <td className="px-4 py-2 text-sm text-gray-800">
<tr className='border-t'> {detailLogData.customer.fullname}
<td className="px-4 py-2 text-sm text-gray-500">Payment Request</td> </td>
<td className="px-4 py-2 text-sm text-gray-500"><pre className="whitespace-pre-wrap break-words max-w-full overflow-auto bg-gray-100 p-2 rounded text-sm">{detailLogData.payment_request}</pre></td> </tr>
</tr> {detailLogData.customer.email && (
<tr className='border-t'> <tr className="border-t">
<td className="px-4 py-2 text-sm text-gray-500">Payment Response</td> <td className="px-4 py-2 text-sm text-gray-500">Email</td>
<td className="px-4 py-2 text-sm text-gray-500"><pre className="whitespace-pre-wrap break-words max-w-full overflow-auto bg-gray-100 p-2 rounded text-sm">{detailLogData.payment_response}</pre></td> <td className="px-4 py-2 text-sm text-gray-800">
</tr> {detailLogData.customer.email}
<tr className='border-t'> </td>
<td className="px-4 py-2 text-sm text-gray-500">Status</td> </tr>
<td className="px-4 py-2 text-sm text-gray-500">{renderStatusBadge(detailLogData.status)}</td> )}
</tr> {detailLogData.customer.bank_name && (
<tr className='border-t'> <tr className="border-t">
<td className="px-4 py-2 text-sm text-gray-500">Prosess Date</td> <td className="px-4 py-2 text-sm text-gray-500">Bank Name</td>
<td className="px-4 py-2 text-sm text-gray-500">{detailLogData.request_date && moment(detailLogData.request_date).isValid() ? moment(detailLogData.request_date).format('DD/MM/YYYY HH:mm:ss') : '-'}</td> <td className="px-4 py-2 text-sm text-gray-800">
</tr> {detailLogData.customer.bank_name}
<tr className='border-t'> </td>
<td className="px-4 py-2 text-sm text-gray-500">Response Date</td> </tr>
<td className="px-4 py-2 text-sm text-gray-500">{detailLogData.response_date && moment(detailLogData.response_date).isValid() ? moment(detailLogData.response_date).format('DD/MM/YYYY HH:mm:ss') : '-'}</td> )}
</tr> {detailLogData.customer.bank_account && (
<tr className='border-t'> <tr className="border-t">
<td className="px-4 py-2 text-sm text-gray-500">Reference Number</td> <td className="px-4 py-2 text-sm text-gray-500">Bank Account</td>
<td className="px-4 py-2 text-sm text-gray-500">{detailLogData.reference}</td> <td className="px-4 py-2 text-sm text-gray-800">
</tr> {detailLogData.customer.bank_account}
</tbody> </td>
</table> </tr>
</div> )}
</div> </>
</div> )}
) : (<div></div>)}
</DialogBody> <tr className="border-t">
</DialogContent> <td className="px-4 py-2 text-sm text-gray-500">Amount</td>
<td className="px-4 py-2 text-sm text-gray-800">{detailLogData.amount}</td>
</tr>
<tr className="border-t">
<td className="px-4 py-2 text-sm text-gray-500">Remark</td>
<td className="px-4 py-2 text-sm text-gray-800">{detailLogData.remark}</td>
</tr>
<tr className="border-t">
<td className="px-4 py-2 text-sm text-gray-500">Remark 1</td>
<td className="px-4 py-2 text-sm text-gray-800">
{detailLogData.remark_1}
</td>
</tr>
<tr className="border-t">
<td className="px-4 py-2 text-sm text-gray-500">Remark 2</td>
<td className="px-4 py-2 text-sm text-gray-800">
{detailLogData.remark_2}
</td>
</tr>
<tr className="border-t">
<td className="px-4 py-2 text-sm text-gray-500">Remark 3</td>
<td className="px-4 py-2 text-sm text-gray-800">
{detailLogData.remark_3}
</td>
</tr>
<tr className="border-t">
<td className="px-4 py-2 text-sm text-gray-500">Reference Number</td>
<td className="px-4 py-2 text-sm text-gray-800">
{detailLogData.reference ?? '-'}
</td>
</tr>
<tr className="border-t">
<td className="px-4 py-2 text-sm text-gray-500">Payment Request</td>
<td className="px-4 py-2 text-sm text-gray-800">
<pre className="whitespace-pre-wrap break-words max-w-full overflow-auto bg-gray-100 p-2 rounded text-sm">
{detailLogData.payment_request ?? '-'}
</pre>
</td>
</tr>
<tr className="border-t">
<td className="px-4 py-2 text-sm text-gray-500">Payment Response</td>
<td className="px-4 py-2 text-sm text-gray-800">
<pre className="whitespace-pre-wrap break-words max-w-full overflow-auto bg-gray-100 p-2 rounded text-sm">
{detailLogData.payment_response ?? '-'}
</pre>
</td>
</tr>
<tr className="border-t">
<td className="px-4 py-2 text-sm text-gray-500">Status</td>
<td className="px-4 py-2 text-sm text-gray-800">
{renderStatusBadge(detailLogData.status)}
</td>
</tr>
<tr className="border-t">
<td className="px-4 py-2 text-sm text-gray-500">Request Date</td>
<td className="px-4 py-2 text-sm text-gray-800">
{detailLogData.request_date &&
moment(detailLogData.request_date).isValid()
? moment(detailLogData.request_date).format('DD/MM/YYYY HH:mm:ss')
: '-'}
</td>
</tr>
<tr className="border-t">
<td className="px-4 py-2 text-sm text-gray-500">Response Date</td>
<td className="px-4 py-2 text-sm text-gray-800">
{detailLogData.response_date &&
moment(detailLogData.response_date).isValid()
? moment(detailLogData.response_date).format('DD/MM/YYYY HH:mm:ss')
: '-'}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
) : (
<div className="text-center text-sm text-gray-400 py-10">No data available</div>
)}
</DialogBody>
</DialogContent>
</Dialog> </Dialog>
); );
}; };

View File

@ -9,23 +9,20 @@ const ListToolbar = () => {
const { table, reload } = useDataGrid(); const { table, reload } = useDataGrid();
const { handleUploadBatchDialog } = useTransactionContext(); const { handleUploadBatchDialog } = useTransactionContext();
// Set the initial state for trxDate
const [trxDate, settrxDate] = useState({ from: '', to: '' }); const [trxDate, settrxDate] = useState({ from: '', to: '' });
// Function to format date to YYYY-MM-DD
const formatDate = (date: Date): string => { const formatDate = (date: Date): string => {
return date.toISOString().split('T')[0]; return date.toISOString().split('T')[0];
}; };
// useEffect to set the default date values
useEffect(() => { useEffect(() => {
const today = new Date(); const today = new Date();
const nextWeek = new Date(today); const nextWeek = new Date(today);
nextWeek.setDate(today.getDate() + 7); nextWeek.setDate(today.getDate() + 7);
settrxDate({ settrxDate({
from: formatDate(today), // Set 'from' to today from: formatDate(today),
to: formatDate(nextWeek) // Set 'to' to 7 days later to: formatDate(nextWeek)
}); });
}, []); }, []);
@ -40,7 +37,7 @@ const ListToolbar = () => {
const handleExportData = () => { const handleExportData = () => {
const link = document.createElement('a'); const link = document.createElement('a');
link.href = toAbsoluteUrl('/media/file-templates/batch-sample.xlsx'); link.href = toAbsoluteUrl('/media/file-templates/upload-batch-template.xlsx');
link.download = 'upload-batch-template.xlsx'; link.download = 'upload-batch-template.xlsx';
document.body.appendChild(link); document.body.appendChild(link);
link.click(); link.click();

View File

@ -27,23 +27,28 @@ import clsx from 'clsx';
import { RefreshCw } from 'lucide-react'; import { RefreshCw } from 'lucide-react';
const API_URL = apiConfig.service_disbursement; const API_URL = apiConfig.service_disbursement;
const API_URL_TRANSACTION = apiConfig.service_transaction;
interface TransferType {
id: string;
name: string;
type: string;
}
const UploadBatchDialog = () => { const UploadBatchDialog = () => {
const parentRef = useRef<any | null>(null); const parentRef = useRef<any | null>(null);
const { showUploadBatchDialog, handleUploadBatchDialog } = useTransactionContext(); const { showUploadBatchDialog, handleUploadBatchDialog } = useTransactionContext();
const { reload } = useDataGrid(); const { reload } = useDataGrid();
const { PostData, PostDataFile, GetData } = useCallApi(); const { PostDataFile, GetData } = useCallApi();
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const [alert, setAlert] = useState({ const [alert, setAlert] = useState({ show: false, message: '' });
show: false,
message: '' const [transferTypes, setTransferTypes] = useState<TransferType[]>([]);
});
const initialState: { const initialState = {
execution_date: string;
file: File | null;
} = {
execution_date: '', execution_date: '',
file: null file: null as File | null,
id_transaction_type: ''
}; };
const [formField, setFormField] = useState(initialState); const [formField, setFormField] = useState(initialState);
@ -52,14 +57,17 @@ const UploadBatchDialog = () => {
setAlert({ show: false, message: '' }); setAlert({ show: false, message: '' });
}; };
/* actions */
const doUploadBatch = useCallback( const doUploadBatch = useCallback(
async (e: React.FormEvent<HTMLFormElement>) => { async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
setIsSubmitting(true); setIsSubmitting(true);
const formData = new FormData(); const formData = new FormData();
formData.append('execution_date', formField.execution_date); const localDate = new Date(formField.execution_date);
const newDate = localDate.toISOString();
formData.append('execution_date', newDate);
formData.append('id_transaction_type', formField.id_transaction_type);
if (formField.file) { if (formField.file) {
formData.append('file', formField.file); formData.append('file', formField.file);
@ -67,9 +75,7 @@ const UploadBatchDialog = () => {
try { try {
const response = await PostDataFile(`${API_URL}/upload-excel`, formData, { const response = await PostDataFile(`${API_URL}/upload-excel`, formData, {
headers: { headers: { 'Content-Type': 'multipart/form-data' }
'Content-Type': 'multipart/form-data'
}
}); });
if (response?.status) { if (response?.status) {
@ -102,26 +108,48 @@ const UploadBatchDialog = () => {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
console.log('Form data before submit:', formField); if (
formField.execution_date.trim() === '' ||
if (formField.execution_date.trim() === '' || formField.file === null) { formField.file === null ||
formField.id_transaction_type.trim() === ''
) {
setAlert({ show: true, message: 'Please fill in all required fields.' }); setAlert({ show: true, message: 'Please fill in all required fields.' });
return; return;
} }
doUploadBatch(e); doUploadBatch(e);
// console.log(formField);
setAlert({ show: false, message: '' }); setAlert({ show: false, message: '' });
}; };
useEffect(() => { useEffect(() => {
if (showUploadBatchDialog === false) { if (showUploadBatchDialog === false) {
resetForm(); resetForm();
return;
} }
const fetchTransferTypes = async () => {
try {
const response = await GetData(`${API_URL_TRANSACTION}/transactiontype/list`, {
limit: 100,
page: 1,
with_deleted: false,
order_field: 'id',
order_direction: 'ASC',
filter: JSON.stringify({ type: "D" })
});
const records: TransferType[] = response?.data?.list ?? [];
setTransferTypes(records);
} catch (error) {
console.error('Failed to fetch transfer types', error);
}
};
fetchTransferTypes();
}, [showUploadBatchDialog]); }, [showUploadBatchDialog]);
return ( return (
<Dialog open={showUploadBatchDialog} onOpenChange={(open) => handleUploadBatchDialog(open)}> <Dialog open={showUploadBatchDialog} onOpenChange={handleUploadBatchDialog}>
<DialogContent className="container-fixed max-w-[768px] flex flex-col p-5 overflow-hidden [&>button]:hidden"> <DialogContent className="container-fixed max-w-[768px] flex flex-col p-5 overflow-hidden [&>button]:hidden">
<DialogHeader className="p-0 border-0"> <DialogHeader className="p-0 border-0">
<DialogTitle></DialogTitle> <DialogTitle></DialogTitle>
@ -129,7 +157,6 @@ const UploadBatchDialog = () => {
<div className="flex items-center justify-between flex-wrap grow"> <div className="flex items-center justify-between flex-wrap grow">
<div className="flex flex-col justify-center"> <div className="flex flex-col justify-center">
<h1 className="text-xl font-semibold leading-none text-gray-900">Upload Batch</h1> <h1 className="text-xl font-semibold leading-none text-gray-900">Upload Batch</h1>
<div className="flex items-center gap-2 text-sm font-normal text-gray-700"></div>
</div> </div>
<div <div
className="cursor-pointer hover:opacity-100 opacity-50" className="cursor-pointer hover:opacity-100 opacity-50"
@ -149,7 +176,7 @@ const UploadBatchDialog = () => {
<h3>{alert.message}</h3> <h3>{alert.message}</h3>
</Alert> </Alert>
)} )}
<form action="" onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div className="card-body grid gap-5 p-0"> <div className="card-body grid gap-5 p-0">
<div className="w-full"> <div className="w-full">
<div className="flex items-baseline flex-wrap lg:flex-nowrap gap-2.5"> <div className="flex items-baseline flex-wrap lg:flex-nowrap gap-2.5">
@ -182,6 +209,31 @@ const UploadBatchDialog = () => {
/> />
</div> </div>
</div> </div>
<div className="w-full">
<div className="flex items-baseline flex-wrap lg:flex-nowrap gap-2.5">
<label className="form-label flex items-center gap-1 max-w-56">
Transfer Type
</label>
<Select
required
value={formField.id_transaction_type}
onValueChange={(val) =>
setFormField((prev) => ({ ...prev, id_transaction_type: val }))
}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select Transfer Type" />
</SelectTrigger>
<SelectContent>
{transferTypes.map((item) => (
<SelectItem key={item.id} value={item.id}>
{item.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="flex justify-end pt-2.5"> <div className="flex justify-end pt-2.5">
<Button variant="default" type="submit" disabled={isSubmitting}> <Button variant="default" type="submit" disabled={isSubmitting}>
{isSubmitting ? ( {isSubmitting ? (
@ -200,4 +252,4 @@ const UploadBatchDialog = () => {
); );
}; };
export { UploadBatchDialog }; export { UploadBatchDialog };

View File

@ -36,22 +36,22 @@ interface ContextProps {
// handleDetailLogDialog: (show: boolean) => void; // handleDetailLogDialog: (show: boolean) => void;
setShowDetailLogDialog: React.Dispatch<React.SetStateAction<boolean>>; setShowDetailLogDialog: React.Dispatch<React.SetStateAction<boolean>>;
setDetailLogData: React.Dispatch<React.SetStateAction<any | null>>; setDetailLogData: React.Dispatch<React.SetStateAction<any | null>>;
detailLogData: any | null detailLogData: any | null;
} }
const initialProps: ContextProps = { const initialProps: ContextProps = {
getTransactionLists: async () => ({ data: [], totalCount: 0 }), getTransactionLists: async () => ({ data: [], totalCount: 0 }),
showDetailDialog: false, showDetailDialog: false,
setShowDetailDialog: () => { }, setShowDetailDialog: () => {},
selectedTransactionId: null, selectedTransactionId: null,
setSelectedTransactionId: () => { }, setSelectedTransactionId: () => {},
showUploadBatchDialog: false, showUploadBatchDialog: false,
handleUploadBatchDialog: (show: boolean) => {}, handleUploadBatchDialog: (show: boolean) => {},
showDetailLogDialog: false, showDetailLogDialog: false,
// handleDetailLogDialog: (show: boolean) => {}, // handleDetailLogDialog: (show: boolean) => {},
setShowDetailLogDialog: () => { }, setShowDetailLogDialog: () => {},
setDetailLogData: () => {}, setDetailLogData: () => {},
detailLogData: null, detailLogData: null
}; };
type StatusCode = 'W' | 'P' | 'F' | 'D' | 'Y'; type StatusCode = 'W' | 'P' | 'F' | 'D' | 'Y';
@ -67,10 +67,9 @@ const statusMap: Record<StatusCode, StatusInfo> = {
P: { label: 'Pending', bg: 'bg-blue-100', text: 'text-blue-600' }, P: { label: 'Pending', bg: 'bg-blue-100', text: 'text-blue-600' },
F: { label: 'Fail', bg: 'bg-red-100', text: 'text-red-600' }, F: { label: 'Fail', bg: 'bg-red-100', text: 'text-red-600' },
D: { label: 'Done', bg: 'bg-green-100', text: 'text-green-600' }, D: { label: 'Done', bg: 'bg-green-100', text: 'text-green-600' },
Y: { label: 'Active', bg: 'bg-green-100', text: 'text-green-600' }, Y: { label: 'Active', bg: 'bg-green-100', text: 'text-green-600' }
}; };
const ManageTransactionContext = createContext<ContextProps>(initialProps); const ManageTransactionContext = createContext<ContextProps>(initialProps);
const API_URL = apiConfig.service_disbursement; const API_URL = apiConfig.service_disbursement;
@ -83,7 +82,7 @@ const TransactionProvider = ({ children }: { children: React.ReactNode }) => {
const [transaction, setTransaction] = useState<TransactionProps[]>([]); const [transaction, setTransaction] = useState<TransactionProps[]>([]);
const { GetData } = useCallApi(); const { GetData } = useCallApi();
const navigate = useNavigate(); const navigate = useNavigate();
const handleUploadBatchDialog = useCallback((show: boolean) => { const handleUploadBatchDialog = useCallback((show: boolean) => {
setShowUploadBatchDialog(show); setShowUploadBatchDialog(show);
}, []); }, []);
@ -100,19 +99,21 @@ const TransactionProvider = ({ children }: { children: React.ReactNode }) => {
enableHiding: false, enableHiding: false,
meta: { meta: {
headerClassName: 'w-[250px]' headerClassName: 'w-[250px]'
}, }
}, },
{ {
accessorFn: (row) => { accessorFn: (row) => {
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(row.amount); return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(
row.amount
);
}, },
id: 'amount', id: 'amount',
header: ({ column }) => <DataGridColumnHeader title="Amount" column={column} />, header: ({ column }) => <DataGridColumnHeader title="Amount" column={column} />,
enableSorting: false, enableSorting: false,
enableHiding: false, enableHiding: false,
meta: { meta: {
headerClassName: 'w-[250px]', headerClassName: 'w-[250px]'
}, }
}, },
{ {
accessorKey: 'total_record', accessorKey: 'total_record',
@ -121,7 +122,7 @@ const TransactionProvider = ({ children }: { children: React.ReactNode }) => {
enableHiding: false, enableHiding: false,
meta: { meta: {
headerClassName: 'w-[250px]' headerClassName: 'w-[250px]'
}, }
}, },
{ {
accessorKey: 'total_success', accessorKey: 'total_success',
@ -130,7 +131,7 @@ const TransactionProvider = ({ children }: { children: React.ReactNode }) => {
enableHiding: false, enableHiding: false,
meta: { meta: {
headerClassName: 'w-[250px]' headerClassName: 'w-[250px]'
}, }
}, },
{ {
accessorKey: 'total_fail', accessorKey: 'total_fail',
@ -139,7 +140,7 @@ const TransactionProvider = ({ children }: { children: React.ReactNode }) => {
enableHiding: false, enableHiding: false,
meta: { meta: {
headerClassName: 'w-[250px]' headerClassName: 'w-[250px]'
}, }
}, },
{ {
accessorKey: 'total_pending', accessorKey: 'total_pending',
@ -148,7 +149,7 @@ const TransactionProvider = ({ children }: { children: React.ReactNode }) => {
enableHiding: false, enableHiding: false,
meta: { meta: {
headerClassName: 'w-[250px]' headerClassName: 'w-[250px]'
}, }
}, },
{ {
accessorFn: (row) => row.status, accessorFn: (row) => row.status,
@ -161,21 +162,19 @@ const TransactionProvider = ({ children }: { children: React.ReactNode }) => {
const { label, bg, text } = statusMap[status] ?? { const { label, bg, text } = statusMap[status] ?? {
label: 'Unknown', label: 'Unknown',
bg: 'bg-gray-100', bg: 'bg-gray-100',
text: 'text-gray-600', text: 'text-gray-600'
}; };
return ( return (
<span <span className={`px-2 py-1 text-xs font-semibold rounded-full ${bg} ${text}`}>
className={`px-2 py-1 text-xs font-semibold rounded-full ${bg} ${text}`}
>
{label} {label}
</span> </span>
); );
}, },
meta: { meta: {
headerClassName: 'w-[100px]', headerClassName: 'w-[100px]',
cellClassName: 'text-center', cellClassName: 'text-center'
}, }
}, },
{ {
accessorFn: (row) => row.execution_date, accessorFn: (row) => row.execution_date,
@ -183,7 +182,7 @@ const TransactionProvider = ({ children }: { children: React.ReactNode }) => {
header: ({ column }) => <DataGridColumnHeader title="Execution Date" column={column} />, header: ({ column }) => <DataGridColumnHeader title="Execution Date" column={column} />,
enableSorting: true, enableSorting: true,
enableHiding: false, enableHiding: false,
cell: ({ row }) => moment(row.original.execution_date).format('DD/MM/YYYY HH:mm:ss') cell: ({ row }) => moment(row.original.execution_date).format('DD/MM/YYYY HH:mm')
}, },
{ {
accessorFn: (row) => row.done_date, accessorFn: (row) => row.done_date,
@ -191,7 +190,8 @@ const TransactionProvider = ({ children }: { children: React.ReactNode }) => {
header: ({ column }) => <DataGridColumnHeader title="Done Date" column={column} />, header: ({ column }) => <DataGridColumnHeader title="Done Date" column={column} />,
enableSorting: true, enableSorting: true,
enableHiding: false, enableHiding: false,
cell: ({ row }) => row.original.done_date ? moment(row.original.done_date).format('DD/MM/YYYY HH:mm:ss') : '' cell: ({ row }) =>
row.original.done_date ? moment(row.original.done_date).format('DD/MM/YYYY HH:mm') : ''
}, },
{ {
id: 'actions', id: 'actions',
@ -220,7 +220,8 @@ const TransactionProvider = ({ children }: { children: React.ReactNode }) => {
} }
} }
], ],
[]); []
);
const getTransactionLists = async (page: number, limit: number, sorting: any, filter: any) => { const getTransactionLists = async (page: number, limit: number, sorting: any, filter: any) => {
try { try {
@ -240,15 +241,13 @@ const TransactionProvider = ({ children }: { children: React.ReactNode }) => {
enddate = filter[0].value.to; enddate = filter[0].value.to;
} }
formattedFilter = { formattedFilter = {};
};
const response = await GetData(`${API_URL}/transaction/history`, { const response = await GetData(`${API_URL}/transaction/history`, {
limit, limit,
page: page + 1, page: page + 1,
with_deleted: false, with_deleted: false,
order_field: "execution_date", order_field: 'execution_date',
order_direction: 'DESC', order_direction: 'DESC',
filter: JSON.stringify(formattedFilter) filter: JSON.stringify(formattedFilter)
}); });

View File

@ -293,7 +293,7 @@ const AddDialog = () => {
<div className="w-full"> <div className="w-full">
<div className="flex items-baseline flex-wrap lg:flex-nowrap gap-2.5"> <div className="flex items-baseline flex-wrap lg:flex-nowrap gap-2.5">
<label className="form-label flex items-center gap-1 max-w-56"> <label className="form-label flex items-center gap-1 max-w-56">
Credit Limit<span className="text-red-500">*</span> Credit Limit per Trx<span className="text-red-500">*</span>
</label> </label>
<div className="w-full"> <div className="w-full">
<NumericFormat <NumericFormat

View File

@ -302,7 +302,7 @@ const EditDialog = () => {
<div className="w-full"> <div className="w-full">
<div className="flex items-baseline flex-wrap lg:flex-nowrap gap-2.5"> <div className="flex items-baseline flex-wrap lg:flex-nowrap gap-2.5">
<label className="form-label flex items-center gap-1 max-w-56"> <label className="form-label flex items-center gap-1 max-w-56">
Credit Limit<span className="text-red-500">*</span> Credit Limit per Trx<span className="text-red-500">*</span>
</label> </label>
<NumericFormat <NumericFormat
className="input" className="input"

View File

@ -115,7 +115,7 @@ const ManageWalletRuleContextProvider = ({ children }: { children: React.ReactNo
{ {
accessorFn: (row) => row.credit_limit, accessorFn: (row) => row.credit_limit,
id: 'credit_limit', id: 'credit_limit',
header: ({ column }) => <DataGridColumnHeader title="Credit Limit" column={column} />, header: ({ column }) => <DataGridColumnHeader title="Credit Limit per Trx" column={column} />,
enableSorting: true, enableSorting: true,
enableHiding: false, enableHiding: false,
cell: ({ row }) => currencyFormat(row.original.credit_limit), cell: ({ row }) => currencyFormat(row.original.credit_limit),

View File

@ -71,6 +71,7 @@ const AgentBalance = () => {
let temp = 1; let temp = 1;
let resBalance = balances.data.data.list.map((el: any) => { let resBalance = balances.data.data.list.map((el: any) => {
el.no = temp++; el.no = temp++;
if (!el.agent_name) el.agent_name = ''
return el; return el;
}); });
setDataBalance(resBalance); setDataBalance(resBalance);

View File

@ -18,6 +18,7 @@ import { useCallApi } from '@/hooks';
import { Checkbox } from '@/components/ui/checkbox'; import { Checkbox } from '@/components/ui/checkbox';
import { doSaveLogActivity } from '@/actions/GlobalActions'; import { doSaveLogActivity } from '@/actions/GlobalActions';
import { set } from 'date-fns'; import { set } from 'date-fns';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
const API_URL = apiConfig.service_dashboard; const API_URL = apiConfig.service_dashboard;
@ -61,7 +62,8 @@ const MenuItemComponent: React.FC<{
}; };
const initialState = { const initialState = {
name: '' name: '',
status_balance: '',
}; };
const AddDialog = () => { const AddDialog = () => {
@ -86,13 +88,15 @@ const AddDialog = () => {
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => { const resetForm = () => {
setFormField(() => ({ name: '' })); setFormField(initialState);
setErrors(() => ({})); setErrors(() => ({}));
setSelectMenus([]); setSelectMenus([]);
}; };
const validateForm = () => { const validateForm = () => {
const requiredFields = [{ key: 'name', label: 'Position Name' }]; const requiredFields = [
{ key: 'name', label: 'Position Name' },
{ key: 'status_balance', label: 'Status Balance' }];
const newErrors: Record<string, string> = {}; const newErrors: Record<string, string> = {};
let isValid = true; let isValid = true;
requiredFields.forEach(({ key, label }) => { requiredFields.forEach(({ key, label }) => {
@ -128,7 +132,8 @@ const AddDialog = () => {
const response = await PostData(`${API_URL}/user_role/create`, { const response = await PostData(`${API_URL}/user_role/create`, {
name: formField.name, name: formField.name,
roles: selectMenus, roles: selectMenus,
status: 'Y' status: 'Y',
status_balance: formField.status_balance
}); });
if (response?.status) { if (response?.status) {
@ -203,6 +208,33 @@ const AddDialog = () => {
</div> </div>
</div> </div>
</div> </div>
<div className="w-full">
<div className="flex items-baseline flex-wrap lg:flex-nowrap gap-2.5">
<label className="form-label flex items-center gap-1 max-w-56">
Status Balance <span className="text-danger">*</span>
</label>
<div className="grow flex flex-col">
<Select
value={formField.status_balance}
onValueChange={(value) => {
setFormField((prev) => ({ ...prev, status_balance: value }));
setErrors((prev) => ({ ...prev, status_balance: '' }));
}}
>
<SelectTrigger className={errors.status ? 'border-red-500' : ''}>
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Y">Active</SelectItem>
<SelectItem value="N">Inactive</SelectItem>
</SelectContent>
</Select>
{errors.status && (
<span className="text-red-500 text-xs mt-1">{errors.status}</span>
)}
</div>
</div>
</div>
<div className="grid md:grid-cols-3 w-full gap-5"> <div className="grid md:grid-cols-3 w-full gap-5">
{menus.map((menu) => ( {menus.map((menu) => (

View File

@ -79,11 +79,15 @@ const EditDialog = () => {
const [selectMenus, setSelectMenus] = useState<string[]>([]); const [selectMenus, setSelectMenus] = useState<string[]>([]);
const [formField, setFormField] = useState({ const [formField, setFormField] = useState({
name: '', name: '',
status: '' status: '',
status_balance: ''
}); });
const [errors, setErrors] = useState<Record<string, string>>({}); const [errors, setErrors] = useState<Record<string, string>>({});
const validateForm = () => { const validateForm = () => {
const requiredFields = [{ key: 'name', label: 'Position Name' }]; const requiredFields = [
{ key: 'name', label: 'Position Name' },
{ key: 'status_balance', label: 'Status Balance' }
];
const newErrors: Record<string, string> = {}; const newErrors: Record<string, string> = {};
let isValid = true; let isValid = true;
requiredFields.forEach(({ key, label }) => { requiredFields.forEach(({ key, label }) => {
@ -131,7 +135,8 @@ const EditDialog = () => {
const response = await PutData(`${API_URL}/user_role/update/${selectedPosition.id}`, { const response = await PutData(`${API_URL}/user_role/update/${selectedPosition.id}`, {
name: formField.name, name: formField.name,
roles: selectMenus, roles: selectMenus,
status: formField.status status: formField.status,
status_balance: formField.status_balance
}); });
if (response?.status) { if (response?.status) {
@ -163,7 +168,8 @@ const EditDialog = () => {
setFormField((prev) => ({ setFormField((prev) => ({
...prev, ...prev,
name: selectedPosition.name, name: selectedPosition.name,
status: selectedPosition.status status: selectedPosition.status,
status_balance: selectedPosition.status_balance
})); }));
setSelectMenus(selectedPosition.roles); setSelectMenus(selectedPosition.roles);
@ -236,6 +242,26 @@ const EditDialog = () => {
</div> </div>
</div> </div>
</div> </div>
<div className="w-full">
<div className="flex items-center flex-wrap gap-2.5">
<label className="form-label max-w-56">Status Balance</label>
<div className="grow">
<Select
value={formField.status_balance}
onValueChange={(status_balance) => setFormField((prev) => ({ ...prev, status_balance }))}
>
<SelectTrigger>
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Y">Active</SelectItem>
<SelectItem value="N">Non Active</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
<div className="grid md:grid-cols-3 w-full gap-5"> <div className="grid md:grid-cols-3 w-full gap-5">
{menus.map((menu) => ( {menus.map((menu) => (

View File

@ -23,6 +23,7 @@ interface selectedPosition {
name: string; name: string;
roles: string[]; roles: string[];
status: string; status: string;
status_balance: string;
} }
const initialProps: ContextProps = { const initialProps: ContextProps = {

View File

@ -107,6 +107,35 @@ const DetailApprovalTransaction = () => {
return null; return null;
}; };
// const [formattedJson, setFormattedJson] = useState('');
// const [highlightedJson, setHighlightedJson] = useState('');
// useEffect(() => {
// const obj = { a: 1, 'b': 'foo', c: [false, 'false', null, 'null', { d: { e: 1.3e5, f: '1.3e5' } }] };
// const str = JSON.stringify(obj, undefined, 4);
// setFormattedJson(str);
// setHighlightedJson(syntaxHighlight(str));
// }, []);
// function syntaxHighlight(json: any) {
// json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
// return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])"(\s:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match: any) {
// let cls = 'number';
// if (/^"/.test(match)) {
// if (/:$/.test(match)) {
// cls = 'key';
// } else {
// cls = 'string';
// }
// } else if (/true|false/.test(match)) {
// cls = 'boolean';
// } else if (/null/.test(match)) {
// cls = 'null';
// }
// return '<span className="' + cls + '">' + match + '</span>';
// });
// }
return ( return (
<Dialog open={showDetailDialog} onOpenChange={setShowDetailDialog}> <Dialog open={showDetailDialog} onOpenChange={setShowDetailDialog}>
@ -223,6 +252,7 @@ const DetailApprovalTransaction = () => {
{(() => { {(() => {
let status; let status;
let badgeClass; let badgeClass;
if (transactionDetails?.status === 'C') { if (transactionDetails?.status === 'C') {
status = 'COMPLETE'; status = 'COMPLETE';
badgeClass = 'bg-green-100 text-green-800'; badgeClass = 'bg-green-100 text-green-800';
@ -232,9 +262,15 @@ const DetailApprovalTransaction = () => {
} else if (transactionDetails?.status === 'O') { } else if (transactionDetails?.status === 'O') {
status = 'ON PROCESS'; status = 'ON PROCESS';
badgeClass = 'bg-blue-100 text-blue-800'; badgeClass = 'bg-blue-100 text-blue-800';
} else { } else if (transactionDetails?.status === 'P') {
status = 'PENDING'; status = 'PENDING';
badgeClass = 'bg-gray-100 text-gray-800'; badgeClass = 'bg-gray-100 text-gray-800';
} else if (transactionDetails?.status === 'R') {
status = 'ROLLBACK';
badgeClass = 'bg-yellow-100 text-yellow-800';
} else if (transactionDetails?.status === 'S') {
status = 'REVERSAL';
badgeClass = 'bg-purple-100 text-purple-800';
} }
return ( return (
@ -261,7 +297,7 @@ const DetailApprovalTransaction = () => {
kind = 'TOP UP'; kind = 'TOP UP';
} else if (transactionDetails?.kind === 'R') { } else if (transactionDetails?.kind === 'R') {
kind = 'RETURN'; kind = 'RETURN';
}else if (transactionDetails?.kind === 'E') { } else if (transactionDetails?.kind === 'E') {
kind = 'REWARD'; kind = 'REWARD';
} }
return kind; return kind;
@ -419,16 +455,33 @@ const DetailApprovalTransaction = () => {
<p className="font-medium"> <p className="font-medium">
{(() => { {(() => {
let status; let status;
let badgeClass;
if (transactionDetails?.status === 'C') { if (transactionDetails?.status === 'C') {
status = 'COMPLETE'; status = 'COMPLETE';
badgeClass = 'bg-green-100 text-green-800';
} else if (transactionDetails?.status === 'F') { } else if (transactionDetails?.status === 'F') {
status = 'FAILED'; status = 'FAILED';
badgeClass = 'bg-red-100 text-red-800';
} else if (transactionDetails?.status === 'O') { } else if (transactionDetails?.status === 'O') {
status = 'ON PROCESS'; status = 'ON PROCESS';
} else { badgeClass = 'bg-blue-100 text-blue-800';
} else if (transactionDetails?.status === 'P') {
status = 'PENDING'; status = 'PENDING';
badgeClass = 'bg-gray-100 text-gray-800';
} else if (transactionDetails?.status === 'R') {
status = 'ROLLBACK';
badgeClass = 'bg-yellow-100 text-yellow-800';
} else if (transactionDetails?.status === 'S') {
status = 'REVERSAL';
badgeClass = 'bg-purple-100 text-purple-800';
} }
return status;
return (
<span className={`px-3 py-1 rounded-full text-xs font-semibold ${badgeClass}`}>
{status}
</span>
);
})()} })()}
</p> </p>
</div> </div>
@ -447,7 +500,7 @@ const DetailApprovalTransaction = () => {
kind = 'TOP UP'; kind = 'TOP UP';
} else if (transactionDetails?.kind === 'R') { } else if (transactionDetails?.kind === 'R') {
kind = 'RETURN'; kind = 'RETURN';
}else if (transactionDetails?.kind === 'E') { } else if (transactionDetails?.kind === 'E') {
kind = 'REWARD'; kind = 'REWARD';
} }
return kind; return kind;
@ -630,11 +683,11 @@ const DetailApprovalTransaction = () => {
<table className="min-w-full table-auto"> <table className="min-w-full table-auto">
<thead> <thead>
<tr className="bg-gray-100"> <tr className="bg-gray-100">
<th className="px-4 py-2 text-left text-sm text-gray-500">Type</th> <th className="px-4 py-2 text-left text-sm text-gray-500">Status</th>
<th className="px-4 py-2 text-left text-sm text-gray-500">Request Date</th> <th className="px-4 py-2 text-left text-sm text-gray-500">Request Date</th>
<th className="px-4 py-2 text-left text-sm text-gray-500">Response Date</th> <th className="px-4 py-2 text-left text-sm text-gray-500">Response Date</th>
<th className="px-4 py-2 text-left text-sm text-gray-500">Request Body</th>
<th className="px-4 py-2 text-left text-sm text-gray-500">Response Body</th> <th className="px-4 py-2 text-left text-sm text-gray-500">Response Body</th>
<th className="px-4 py-2 text-left text-sm text-gray-500">Response Code</th>
<th className="px-4 py-2 text-left text-sm text-gray-500">Request Endpoint</th> <th className="px-4 py-2 text-left text-sm text-gray-500">Request Endpoint</th>
</tr> </tr>
</thead> </thead>
@ -642,7 +695,25 @@ const DetailApprovalTransaction = () => {
{transactionDetails?.log && transactionDetails?.log.length > 0 ? ( {transactionDetails?.log && transactionDetails?.log.length > 0 ? (
transactionDetails.log.map((log: { type: string, request_endpoint: string, status: string; request_date: string; response_date: string; request_body: string; response_body: string; response_code: number }, index: number) => ( transactionDetails.log.map((log: { type: string, request_endpoint: string, status: string; request_date: string; response_date: string; request_body: string; response_body: string; response_code: number }, index: number) => (
<tr key={index} className="border-t"> <tr key={index} className="border-t">
<td className="px-4 py-2 text-sm text-gray-500">{log.type}</td> <td className="px-4 py-2 text-sm text-gray-500">
{(() => {
let status;
if (log.status === 'C') {
status = 'COMPLETE';
} else if (log.status === 'F') {
status = 'FAILED';
} else if (log.status === 'O') {
status = 'ON PROCESS';
} else if (log.status === 'P') {
status = 'PENDING';
} else if (log.status === 'R') {
status = 'ROLLBACK';
} else if (log.status === 'S') {
status = 'REVERSAL';
}
return status;
})()}
</td>
<td className="px-4 py-2 text-sm text-gray-500">{log.request_date <td className="px-4 py-2 text-sm text-gray-500">{log.request_date
? new Date(log.request_date).toLocaleDateString('en-GB', { ? new Date(log.request_date).toLocaleDateString('en-GB', {
day: '2-digit', day: '2-digit',
@ -665,9 +736,40 @@ const DetailApprovalTransaction = () => {
hour12: false hour12: false
}) })
: ''}</td> : ''}</td>
<td className="px-4 py-2 text-sm text-gray-500">{log.request_body}</td> <td className="px-4 py-2 text-sm text-gray-500">
<td className="px-4 py-2 text-sm text-gray-500">{log.response_body}</td> <pre style={{ fontFamily: 'monospace', whiteSpace: 'pre-wrap' }}>
<td className="px-4 py-2 text-sm text-gray-500">{log.request_endpoint}</td> {(() => {
try {
if (log.response_code === null) {
return '';
}
const parsed = JSON.parse(log.request_body);
return JSON.stringify(parsed, null, 2);
} catch (e) {
return log.request_body; // fallback: tampilkan as-is jika gagal parse
}
})()}
</pre>
</td>
<td className="px-4 py-2 text-sm text-gray-500">
<pre style={{ fontFamily: 'monospace', whiteSpace: 'pre-wrap' }}>
{(() => {
try {
if (log.response_body === null) {
return '';
}
const parsed = JSON.parse(log.response_body);
return JSON.stringify(parsed, null, 2) ?? '';
} catch (e) {
return log.response_body ?? ''; // fallback: tampilkan as-is jika gagal parse
}
})()}
</pre>
</td>
<td className="px-4 py-2 text-sm text-gray-500">{log.request_endpoint || ''}</td>
</tr> </tr>
)) ))
) : ( ) : (
@ -756,19 +858,37 @@ const DetailApprovalTransaction = () => {
<table className="min-w-full table-auto"> <table className="min-w-full table-auto">
<thead> <thead>
<tr className="bg-gray-100"> <tr className="bg-gray-100">
<th className="px-4 py-2 text-left text-sm text-gray-500">Type</th> <th className="px-4 py-2 text-left text-sm text-gray-500">Status</th>
<th className="px-4 py-2 text-left text-sm text-gray-500">Request Date</th> <th className="px-4 py-2 text-left text-sm text-gray-500">Request Date</th>
<th className="px-4 py-2 text-left text-sm text-gray-500">Response Date</th> <th className="px-4 py-2 text-left text-sm text-gray-500">Response Date</th>
<th className="px-4 py-2 text-left text-sm text-gray-500">Request Body</th>
<th className="px-4 py-2 text-left text-sm text-gray-500">Response Body</th> <th className="px-4 py-2 text-left text-sm text-gray-500">Response Body</th>
<th className="px-4 py-2 text-left text-sm text-gray-500">Response Code</th>
<th className="px-4 py-2 text-left text-sm text-gray-500">Request Endpoint</th> <th className="px-4 py-2 text-left text-sm text-gray-500">Request Endpoint</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{transactionDetails?.p24 && transactionDetails?.p24.length > 0 ? ( {transactionDetails?.p24 && transactionDetails?.p24.length > 0 ? (
transactionDetails.p24.map((log: { request_endpoint: string, type: string; request_date: string; response_date: string; request_body: string; response_body: string; response_code: number }, index: number) => ( transactionDetails.p24.map((log: { status: string, request_endpoint: string, type: string; request_date: string; response_date: string; request_body: string; response_body: string; response_code: number }, index: number) => (
<tr key={index} className="border-t"> <tr key={index} className="border-t">
<td className="px-4 py-2 text-sm text-gray-500">{log.type}</td> <td className="px-4 py-2 text-sm text-gray-500">
{(() => {
let status;
if (log.status === 'C') {
status = 'COMPLETE';
} else if (log.status === 'F') {
status = 'FAILED';
} else if (log.status === 'O') {
status = 'ON PROCESS';
} else if (log.status === 'P') {
status = 'PENDING';
} else if (log.status === 'R') {
status = 'ROLLBACK';
} else if (log.status === 'S') {
status = 'REVERSAL';
}
return status;
})()}
</td>
<td className="px-4 py-2 text-sm text-gray-500">{log.request_date <td className="px-4 py-2 text-sm text-gray-500">{log.request_date
? new Date(log.request_date).toLocaleDateString('en-GB', { ? new Date(log.request_date).toLocaleDateString('en-GB', {
day: '2-digit', day: '2-digit',
@ -792,9 +912,41 @@ const DetailApprovalTransaction = () => {
hour12: false hour12: false
}) })
: ''}</td> : ''}</td>
<td className="px-4 py-2 text-sm text-gray-500">{log.request_body}</td> <td className="px-4 py-2 text-sm text-gray-500">
<td className="px-4 py-2 text-sm text-gray-500">{log.response_body}</td> <pre style={{ fontFamily: 'monospace', whiteSpace: 'pre-wrap' }}>
<td className="px-4 py-2 text-sm text-gray-500">{log.request_endpoint ?? '-'}</td> {(() => {
try {
if (log.request_body === null) {
return '';
}
const parsed = JSON.parse(log.request_body);
return JSON.stringify(parsed, null, 2) ?? '';
} catch (e) {
return log.request_body ?? ''; // fallback: tampilkan as-is jika gagal parse
}
})()}
</pre>
</td>
<td className="px-4 py-2 text-sm text-gray-500">
<pre style={{ fontFamily: 'monospace', whiteSpace: 'pre-wrap' }}>
{(() => {
try {
if (log.response_code === null) {
return '';
}
const parsed = JSON.parse(log.response_body);
return JSON.stringify(parsed, null, 2);
} catch (e) {
return log.response_body; // fallback: tampilkan as-is jika gagal parse
}
})()}
</pre>
</td>
<td className="px-4 py-2 text-sm text-gray-500">{log.request_endpoint ?? ''}</td>
</tr> </tr>
)) ))
) : ( ) : (

View File

@ -13,6 +13,7 @@ import DetailApprovalTransaction from '../blocks/DetailApprovalTransaction';
import ApprovalDialog from '../blocks/ApprovalDialog'; import ApprovalDialog from '../blocks/ApprovalDialog';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { getAuth } from '@/auth';
interface ApprovalTransactionProps { interface ApprovalTransactionProps {
id: number; id: number;
@ -158,11 +159,15 @@ const ApprovalTransactionProvider = ({ children }: { children: React.ReactNode }
}, },
{ {
accessorFn: (row) => { accessorFn: (row) => {
switch (row.status) { switch (row.status) {
case 'C': return 'COMPLETE'; case 'C': return 'COMPLETE';
case 'F': return 'FAILED'; case 'F': return 'FAILED';
case 'O': return 'ON PROCESS'; case 'O': return 'ON PROCESS';
default: return 'PENDING'; case "P" : return 'PENDING';
case 'R': return 'ROLLBACK';
case 'S': return 'REVERSAL';
default: return 'UNKNOWN';
} }
}, },
id: 'status', id: 'status',

View File

@ -127,6 +127,35 @@ const DetailTransaction = () => {
return null; return null;
}; };
// const [formattedJson, setFormattedJson] = useState('');
// const [highlightedJson, setHighlightedJson] = useState('');
// useEffect(() => {
// const obj = { a: 1, 'b': 'foo', c: [false, 'false', null, 'null', { d: { e: 1.3e5, f: '1.3e5' } }] };
// const str = JSON.stringify(obj, undefined, 4);
// setFormattedJson(str);
// setHighlightedJson(syntaxHighlight(str));
// }, []);
// function syntaxHighlight(json: any) {
// json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
// return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])"(\s:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match: any) {
// let cls = 'number';
// if (/^"/.test(match)) {
// if (/:$/.test(match)) {
// cls = 'key';
// } else {
// cls = 'string';
// }
// } else if (/true|false/.test(match)) {
// cls = 'boolean';
// } else if (/null/.test(match)) {
// cls = 'null';
// }
// return '<span className="' + cls + '">' + match + '</span>';
// });
// }
return ( return (
<Dialog open={showDetailDialog} onOpenChange={setShowDetailDialog}> <Dialog open={showDetailDialog} onOpenChange={setShowDetailDialog}>
<DialogContent className="container-fixed max-w-[1024px] flex flex-col p-5 overflow-hidden"> <DialogContent className="container-fixed max-w-[1024px] flex flex-col p-5 overflow-hidden">
@ -137,12 +166,33 @@ const DetailTransaction = () => {
<DialogBody> <DialogBody>
{/* Tabs Navigation */} {/* Tabs Navigation */}
<div className="flex border-b border-gray-200"> <div className="flex border-b border-gray-200">
{/* {transactionDetails?.kind == 'P' && ( */}
<button <button
className={`py-2 px-4 font-medium text-sm focus:outline-none ${activeTab === 'detail' ? 'text-blue-600 border-b-2 border-blue-600' : 'text-gray-500 hover:text-gray-700'}`} className={`py-2 px-4 font-medium text-sm focus:outline-none ${activeTab === 'detail' ? 'text-blue-600 border-b-2 border-blue-600' : 'text-gray-500 hover:text-gray-700'}`}
onClick={() => setActiveTab('detail')} onClick={() => setActiveTab('detail')}
> >
Detail Transaction Detail Transaction
</button> </button>
{/* )} */}
{transactionDetails?.kind == 'P' && (
<button
className={`py-2 px-4 font-medium text-sm focus:outline-none ${activeTab === 'purchase' ? 'text-blue-600 border-b-2 border-blue-600' : 'text-gray-500 hover:text-gray-700'}`}
onClick={() => setActiveTab('purchase')}
>
Purchase Detail
</button>
)}
{transactionDetails?.kind == 'P' && (
<button
className={`py-2 px-4 font-medium text-sm focus:outline-none ${activeTab === 'productdetail' ? 'text-blue-600 border-b-2 border-blue-600' : 'text-gray-500 hover:text-gray-700'}`}
onClick={() => setActiveTab('productdetail')}
>
Product Detail
</button>
)}
<button <button
className={`py-2 px-4 font-medium text-sm focus:outline-none ${activeTab === 'origincustomer' ? 'text-blue-600 border-b-2 border-blue-600' : 'text-gray-500 hover:text-gray-700'}`} className={`py-2 px-4 font-medium text-sm focus:outline-none ${activeTab === 'origincustomer' ? 'text-blue-600 border-b-2 border-blue-600' : 'text-gray-500 hover:text-gray-700'}`}
onClick={() => setActiveTab('origincustomer')} onClick={() => setActiveTab('origincustomer')}
@ -161,41 +211,141 @@ const DetailTransaction = () => {
> >
Origin Wallet Origin Wallet
</button> </button>
<button {transactionDetails?.kind !== 'P' && (
className={`py-2 px-4 font-medium text-sm focus:outline-none ${activeTab === 'destinationwallet' ? 'text-blue-600 border-b-2 border-blue-600' : 'text-gray-500 hover:text-gray-700'}`} <button
onClick={() => setActiveTab('destinationwallet')} className={`py-2 px-4 font-medium text-sm focus:outline-none ${activeTab === 'destinationwallet' ? 'text-blue-600 border-b-2 border-blue-600' : 'text-gray-500 hover:text-gray-700'}`}
> onClick={() => setActiveTab('destinationwallet')}
Destination Wallet >
</button> Destination Wallet
</button>
)}
<button <button
className={`py-2 px-4 font-medium text-sm focus:outline-none ${activeTab === 'log' ? 'text-blue-600 border-b-2 border-blue-600' : 'text-gray-500 hover:text-gray-700'}`} className={`py-2 px-4 font-medium text-sm focus:outline-none ${activeTab === 'log' ? 'text-blue-600 border-b-2 border-blue-600' : 'text-gray-500 hover:text-gray-700'}`}
onClick={() => setActiveTab('log')} onClick={() => setActiveTab('log')}
> >
Transaction Log Transaction Log
</button> </button>
<button
className={`py-2 px-4 font-medium text-sm focus:outline-none ${activeTab === 'approve' ? 'text-blue-600 border-b-2 border-blue-600' : 'text-gray-500 hover:text-gray-700'}`} {transactionDetails?.kind !== 'P' && (
onClick={() => setActiveTab('approve')} <button
> className={`py-2 px-4 font-medium text-sm focus:outline-none ${activeTab === 'approve' ? 'text-blue-600 border-b-2 border-blue-600' : 'text-gray-500 hover:text-gray-700'}`}
Approval Log onClick={() => setActiveTab('approve')}
</button> >
<button Approval Log
className={`py-2 px-4 font-medium text-sm focus:outline-none ${activeTab === 'p24' ? 'text-blue-600 border-b-2 border-blue-600' : 'text-gray-500 hover:text-gray-700'}`} </button>
onClick={() => setActiveTab('p24')} )}
>
Log P24 {transactionDetails?.kind !== 'P' && (
</button> <button
className={`py-2 px-4 font-medium text-sm focus:outline-none ${activeTab === 'p24' ? 'text-blue-600 border-b-2 border-blue-600' : 'text-gray-500 hover:text-gray-700'}`}
onClick={() => setActiveTab('p24')}
>
Log P24
</button>
)}
</div> </div>
{/* Tab Content */} {activeTab === 'purchase' && (
<>
<h3 className="font-semibold flex items-center mt-4">
Purchase
</h3>
<div className="grid grid-cols-2 gap-4 mt-4">
<div>
<p className="text-sm text-gray-500">Amount</p>
<p className="font-medium">
{new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(
transactionDetails?.purchase?.amount ?? transactionDetails?.transfer?.amount ?? 0
)}
</p>
</div>
<div>
<p className="text-sm text-gray-500">Cashback</p>
<p className="font-medium">
{new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(
transactionDetails?.purchase?.cashback ?? transactionDetails?.transfer?.cashback ?? 0
)}
</p>
</div>
<div>
<p className="text-sm text-gray-500">Cashback Point</p>
<p className="font-medium">
{transactionDetails?.purchase?.cashback_point ?? transactionDetails?.transfer?.cashback_point ?? 0}
</p>
</div>
<div>
<p className="text-sm text-gray-500">Fee Amount</p>
<p className="font-medium">
{new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(
transactionDetails?.purchase?.fee_amount ?? transactionDetails?.transfer?.fee_amount ?? 0
)}
</p>
</div>
</div>
</>
)}
{activeTab === 'productdetail' && (
<>
<div className="space-y-4">
<h3 className="font-semibold flex items-center mt-4">
Product Information
</h3>
<div className="grid grid-cols-2 gap-4 mt-4">
<div>
<p className="text-sm text-gray-500">Product Name</p>
<p className="font-medium">
{transactionDetails?.purchase?.product?.name ?? "-"}
</p>
</div>
<div>
<p className="text-sm text-gray-500">Price Cash</p>
<p className="font-medium">
{transactionDetails?.purchase?.product?.price_cash != null
? new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(transactionDetails.purchase.product.price_cash)
: "-"}
</p>
</div>
<div>
<p className="text-sm text-gray-500">Price Point</p>
<p className="font-medium">
{transactionDetails?.purchase?.product?.price_point ?? "-"}
</p>
</div>
<div>
<p className="text-sm text-gray-500">Product Type</p>
<p className="font-medium">
{transactionDetails?.purchase?.product?.type ?? "-"}
</p>
</div>
<div>
<p className="text-sm text-gray-500">Provider Name</p>
<p className="font-medium">
{transactionDetails?.purchase?.product?.provider?.description ?? "-"}
</p>
</div>
<div>
<p className="text-sm text-gray-500">Provider Type</p>
<p className="font-medium">
{transactionDetails?.purchase?.product?.provider?.type === "h2h"
? "HOST TO HOST"
: transactionDetails?.purchase?.product?.provider?.type === "agent"
? "AGENT"
: "-"}
</p>
</div>
</div>
</div>
</>
)}
<div className="py-4 overflow-y-auto max-h-[400px]"> <div className="py-4 overflow-y-auto max-h-[400px]">
{activeTab === 'detail' && transactionDetails?.kind === 'P' && ( {activeTab === 'detail' && transactionDetails?.kind === 'P' && (
<div className="space-y-4"> <div className="space-y-4">
<h3 className="font-semibold flex items-center"> <h3 className="font-semibold flex items-center">
Transaction Information Transaction Information
<span className="ml-2 bg-blue-100 text-blue-800 text-xs font-semibold mr-2 px-2.5 py-0.5 rounded">
Info
</span>
</h3> </h3>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
@ -217,7 +367,7 @@ const DetailTransaction = () => {
</div> </div>
<div> <div>
<p className="text-sm text-gray-500">Full Name</p> <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>
<div> <div>
<p className="text-sm text-gray-500">Amount</p> <p className="text-sm text-gray-500">Amount</p>
@ -243,6 +393,8 @@ const DetailTransaction = () => {
{(() => { {(() => {
let status; let status;
let badgeClass; let badgeClass;
if (transactionDetails?.status === 'C') { if (transactionDetails?.status === 'C') {
status = 'COMPLETE'; status = 'COMPLETE';
badgeClass = 'bg-green-100 text-green-800'; badgeClass = 'bg-green-100 text-green-800';
@ -252,9 +404,22 @@ const DetailTransaction = () => {
} else if (transactionDetails?.status === 'O') { } else if (transactionDetails?.status === 'O') {
status = 'ON PROCESS'; status = 'ON PROCESS';
badgeClass = 'bg-blue-100 text-blue-800'; badgeClass = 'bg-blue-100 text-blue-800';
} else { } else if (transactionDetails?.status === 'P') {
} else if (transactionDetails?.status === 'P') {
status = 'PENDING'; status = 'PENDING';
badgeClass = 'bg-gray-100 text-gray-800'; badgeClass = 'bg-gray-100 text-gray-800';
} else if (transactionDetails?.status === 'R') {
status = 'ROLLBACK';
badgeClass = 'bg-yellow-100 text-yellow-800';
} else if (transactionDetails?.status === 'S') {
status = 'REVERSAL';
badgeClass = 'bg-purple-100 text-purple-800';
} else if (transactionDetails?.status === 'R') {
status = 'ROLLBACK';
badgeClass = 'bg-yellow-100 text-yellow-800';
} else if (transactionDetails?.status === 'S') {
status = 'REVERSAL';
badgeClass = 'bg-purple-100 text-purple-800';
} }
return ( return (
@ -281,8 +446,16 @@ const DetailTransaction = () => {
kind = 'TOP UP'; kind = 'TOP UP';
} else if (transactionDetails?.kind === 'R') { } else if (transactionDetails?.kind === 'R') {
kind = 'RETURN'; kind = 'RETURN';
}else if (transactionDetails?.kind === 'E') { } else if (transactionDetails?.kind === 'E') {
kind = 'REWARD'; kind = 'REWARD';
} else if (transactionDetails?.kind === 'L') {
kind = 'PURCHASE LOJA';
} else if (transactionDetails?.kind === 'B') {
kind = 'TOP UP P24';
} else if (transactionDetails?.kind === 'A') {
kind = 'TRANSFER AGENT';
} else if (transactionDetails?.kind === 'M') {
kind = 'WITHDRAWAL AGENT';
} }
return kind; return kind;
})()} })()}
@ -298,100 +471,8 @@ const DetailTransaction = () => {
</div> </div>
</div> </div>
<h3 className="font-semibold flex items-center">
Purchase
<span className="ml-2 bg-blue-100 text-blue-800 text-xs font-semibold mr-2 px-2.5 py-0.5 rounded">
Purchase
</span>
</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-gray-500">Amount</p>
<p className="font-medium">
{new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(
transactionDetails?.purchase?.amount ?? transactionDetails?.transfer?.amount ?? 0
)}
</p>
</div>
<div>
<p className="text-sm text-gray-500">Cashback</p>
<p className="font-medium">
{new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(
transactionDetails?.purchase?.cashback ?? transactionDetails?.transfer?.cashback ?? 0
)}
</p>
</div>
<div>
<p className="text-sm text-gray-500">Cashback Point</p>
<p className="font-medium">
{transactionDetails?.purchase?.cashback_point ?? transactionDetails?.transfer?.cashback_point ?? 0}
</p>
</div>
<div>
<p className="text-sm text-gray-500">Fee Amount</p>
<p className="font-medium">
{new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(
transactionDetails?.purchase?.fee_amount ?? transactionDetails?.transfer?.fee_amount ?? 0
)}
</p>
</div>
</div>
</div> </div>
)} )}
{activeTab === 'detail' && (
<div className="space-y-4">
<h3 className="font-semibold flex items-center">
Product Information
<span className="ml-2 bg-blue-100 text-blue-800 text-xs font-semibold mr-2 px-2.5 py-0.5 rounded">
Product Info
</span>
</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-gray-500">Product Name</p>
<p className="font-medium">
{transactionDetails?.purchase?.product?.name ?? "-"}
</p>
</div>
<div>
<p className="text-sm text-gray-500">Price Cash</p>
<p className="font-medium">
{transactionDetails?.purchase?.product?.price_cash != null
? new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(transactionDetails.purchase.product.price_cash)
: "-"}
</p>
</div>
<div>
<p className="text-sm text-gray-500">Price Point</p>
<p className="font-medium">
{transactionDetails?.purchase?.product?.price_point ?? "-"}
</p>
</div>
<div>
<p className="text-sm text-gray-500">Product Type</p>
<p className="font-medium">
{transactionDetails?.purchase?.product?.type ?? "-"}
</p>
</div>
<div>
<p className="text-sm text-gray-500">Provider Name</p>
<p className="font-medium">
{transactionDetails?.purchase?.product?.provider?.description ?? "-"}
</p>
</div>
<div>
<p className="text-sm text-gray-500">Provider Type</p>
<p className="font-medium">
{transactionDetails?.purchase?.product?.provider?.type === "h2h"
? "HOST TO HOST"
: transactionDetails?.purchase?.product?.provider?.type === "agent"
? "AGENT"
: "-"}
</p>
</div>
</div>
</div>
)}
{activeTab === 'detail' && transactionDetails?.kind != 'P' && transactionDetails?.transfer != null && ( {activeTab === 'detail' && transactionDetails?.kind != 'P' && transactionDetails?.transfer != null && (
<div className="space-y-4"> <div className="space-y-4">
@ -418,7 +499,7 @@ const DetailTransaction = () => {
</div> </div>
<div> <div>
<p className="text-sm text-gray-500">Full Name</p> <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>
<div> <div>
<p className="text-sm text-gray-500">Amount</p> <p className="text-sm text-gray-500">Amount</p>
@ -434,18 +515,46 @@ const DetailTransaction = () => {
<p className="text-sm text-gray-500">Status</p> <p className="text-sm text-gray-500">Status</p>
<p className="font-medium"> <p className="font-medium">
{(() => { {(() => {
let status; let status = 'UNKNOWN';
if (transactionDetails?.status === 'C') { let badgeClass = 'bg-gray-100 text-gray-800';
status = 'COMPLETE';
} else if (transactionDetails?.status === 'F') { switch (transactionDetails?.status) {
status = 'FAILED'; case 'C':
} else if (transactionDetails?.status === 'O') { status = 'COMPLETE';
status = 'ON PROCESS'; badgeClass = 'bg-green-100 text-green-800';
} else { break;
status = 'PENDING'; case 'F':
status = 'FAILED';
badgeClass = 'bg-red-100 text-red-800';
break;
case 'O':
status = 'ON PROCESS';
badgeClass = 'bg-blue-100 text-blue-800';
break;
case 'P':
status = 'PENDING';
badgeClass = 'bg-gray-100 text-gray-800';
break;
case 'R':
status = 'ROLLBACK';
badgeClass = 'bg-yellow-100 text-yellow-800';
break;
case 'S':
status = 'REVERSAL';
badgeClass = 'bg-purple-100 text-purple-800';
break;
default:
// optional: keep 'UNKNOWN' or set to null
break;
} }
return status;
return (
<span className={`px-3 py-1 rounded-full text-xs font-semibold ${badgeClass}`}>
{status}
</span>
);
})()} })()}
</p> </p>
</div> </div>
<div> <div>
@ -463,7 +572,8 @@ const DetailTransaction = () => {
kind = 'TOP UP'; kind = 'TOP UP';
} else if (transactionDetails?.kind === 'R') { } else if (transactionDetails?.kind === 'R') {
kind = 'RETURN'; kind = 'RETURN';
}else if (transactionDetails?.kind === 'E') { } else if (transactionDetails?.kind === 'E') {
} else if (transactionDetails?.kind === 'E') {
kind = 'REWARD'; kind = 'REWARD';
} }
return kind; return kind;
@ -501,7 +611,7 @@ const DetailTransaction = () => {
<div> <div>
<p className="text-sm text-gray-500">Phone Number</p> <p className="text-sm text-gray-500">Phone Number</p>
<p className="font-medium">{transactionDetails?.origin_msisdn}</p> <p className="font-medium">{transactionDetails?.origin_msisdn}</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>
<div> <div>
<p className="text-sm text-gray-500">Phone Number</p> <p className="text-sm text-gray-500">Phone Number</p>
@ -592,7 +702,7 @@ const DetailTransaction = () => {
</div> </div>
)} )}
{activeTab === 'destinationwallet' && ( {activeTab === 'destinationwallet' && transactionDetails?.kind?.trim()?.toUpperCase() !== 'P' && (
<div className="space-y-4"> <div className="space-y-4">
<h3 className="font-semibold">Destination Wallet</h3> <h3 className="font-semibold">Destination Wallet</h3>
@ -638,6 +748,7 @@ const DetailTransaction = () => {
)} )}
</div> </div>
)} )}
@ -648,11 +759,11 @@ const DetailTransaction = () => {
<table className="min-w-full table-auto"> <table className="min-w-full table-auto">
<thead> <thead>
<tr className="bg-gray-100"> <tr className="bg-gray-100">
<th className="px-4 py-2 text-left text-sm text-gray-500">Type</th> <th className="px-4 py-2 text-left text-sm text-gray-500">Status</th>
<th className="px-4 py-2 text-left text-sm text-gray-500">Request Date</th> <th className="px-4 py-2 text-left text-sm text-gray-500">Request Date</th>
<th className="px-4 py-2 text-left text-sm text-gray-500">Response Date</th> <th className="px-4 py-2 text-left text-sm text-gray-500">Response Date</th>
<th className="px-4 py-2 text-left text-sm text-gray-500">Request Body</th>
<th className="px-4 py-2 text-left text-sm text-gray-500">Response Body</th> <th className="px-4 py-2 text-left text-sm text-gray-500">Response Body</th>
<th className="px-4 py-2 text-left text-sm text-gray-500">Response Code</th>
<th className="px-4 py-2 text-left text-sm text-gray-500">Request Endpoint</th> <th className="px-4 py-2 text-left text-sm text-gray-500">Request Endpoint</th>
</tr> </tr>
</thead> </thead>
@ -660,7 +771,25 @@ const DetailTransaction = () => {
{transactionDetails?.log && transactionDetails?.log.length > 0 ? ( {transactionDetails?.log && transactionDetails?.log.length > 0 ? (
transactionDetails.log.map((log: { type: string, request_endpoint: string, status: string; request_date: string; response_date: string; request_body: string; response_body: string; response_code: number }, index: number) => ( transactionDetails.log.map((log: { type: string, request_endpoint: string, status: string; request_date: string; response_date: string; request_body: string; response_body: string; response_code: number }, index: number) => (
<tr key={index} className="border-t"> <tr key={index} className="border-t">
<td className="px-4 py-2 text-sm text-gray-500">{log.type}</td> <td className="px-4 py-2 text-sm text-gray-500">
{(() => {
let status;
if (log.status === 'C') {
status = 'COMPLETE';
} else if (log.status === 'F') {
status = 'FAILED';
} else if (log.status === 'O') {
status = 'ON PROCESS';
} else if (log.status === 'P') {
status = 'PENDING';
} else if (log.status === 'R') {
status = 'ROLLBACK';
} else if (log.status === 'S') {
status = 'REVERSAL';
}
return status;
})()}
</td>
<td className="px-4 py-2 text-sm text-gray-500">{log.request_date <td className="px-4 py-2 text-sm text-gray-500">{log.request_date
? new Date(log.request_date).toLocaleDateString('en-GB', { ? new Date(log.request_date).toLocaleDateString('en-GB', {
day: '2-digit', day: '2-digit',
@ -683,9 +812,39 @@ const DetailTransaction = () => {
hour12: false hour12: false
}) })
: ''}</td> : ''}</td>
<td className="px-4 py-2 text-sm text-gray-500">{log.request_body}</td> <td className="px-4 py-2 text-sm text-gray-500">
<td className="px-4 py-2 text-sm text-gray-500">{log.response_body}</td> <pre style={{ fontFamily: 'monospace', whiteSpace: 'pre-wrap' }}>
<td className="px-4 py-2 text-sm text-gray-500">{log.request_endpoint}</td> {(() => {
try {
if (log.response_code === null) {
return '';
}
const parsed = JSON.parse(log.request_body);
return JSON.stringify(parsed, null, 2);
} catch (e) {
return log.request_body; // fallback: tampilkan as-is jika gagal parse
}
})()}
</pre>
</td>
<td className="px-4 py-2 text-sm text-gray-500">
<pre style={{ fontFamily: 'monospace', whiteSpace: 'pre-wrap' }}>
{(() => {
try {
if (log.response_body === null) {
return '';
}
const parsed = JSON.parse(log.response_body);
return JSON.stringify(parsed, null, 2) ?? '';
} catch (e) {
return log.response_body ?? ''; // fallback: tampilkan as-is jika gagal parse
}
})()}
</pre>
</td>
<td className="px-4 py-2 text-sm text-gray-500">{log.request_endpoint || ''}</td>
</tr> </tr>
)) ))
) : ( ) : (
@ -701,7 +860,7 @@ const DetailTransaction = () => {
</div> </div>
)} )}
{activeTab === 'approve' && ( {activeTab === 'approve' && transactionDetails?.kind == "P" && (
<div className="space-y-4"> <div className="space-y-4">
<h3 className="font-semibold">Approval Logs</h3> <h3 className="font-semibold">Approval Logs</h3>
{transactionDetails?.log_approve.length === 0 ? ( {transactionDetails?.log_approve.length === 0 ? (
@ -774,19 +933,37 @@ const DetailTransaction = () => {
<table className="min-w-full table-auto"> <table className="min-w-full table-auto">
<thead> <thead>
<tr className="bg-gray-100"> <tr className="bg-gray-100">
<th className="px-4 py-2 text-left text-sm text-gray-500">Type</th> <th className="px-4 py-2 text-left text-sm text-gray-500">Status</th>
<th className="px-4 py-2 text-left text-sm text-gray-500">Request Date</th> <th className="px-4 py-2 text-left text-sm text-gray-500">Request Date</th>
<th className="px-4 py-2 text-left text-sm text-gray-500">Response Date</th> <th className="px-4 py-2 text-left text-sm text-gray-500">Response Date</th>
<th className="px-4 py-2 text-left text-sm text-gray-500">Request Body</th>
<th className="px-4 py-2 text-left text-sm text-gray-500">Response Body</th> <th className="px-4 py-2 text-left text-sm text-gray-500">Response Body</th>
<th className="px-4 py-2 text-left text-sm text-gray-500">Response Code</th>
<th className="px-4 py-2 text-left text-sm text-gray-500">Request Endpoint</th> <th className="px-4 py-2 text-left text-sm text-gray-500">Request Endpoint</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{transactionDetails?.p24 && transactionDetails?.p24.length > 0 ? ( {transactionDetails?.p24 && transactionDetails?.p24.length > 0 ? (
transactionDetails.p24.map((log: { request_endpoint: string, type: string; request_date: string; response_date: string; request_body: string; response_body: string; response_code: number }, index: number) => ( transactionDetails.p24.map((log: { status: string, request_endpoint: string, type: string; request_date: string; response_date: string; request_body: string; response_body: string; response_code: number }, index: number) => (
<tr key={index} className="border-t"> <tr key={index} className="border-t">
<td className="px-4 py-2 text-sm text-gray-500">{log.type}</td> <td className="px-4 py-2 text-sm text-gray-500">
{(() => {
let status;
if (log.status === 'C') {
status = 'COMPLETE';
} else if (log.status === 'F') {
status = 'FAILED';
} else if (log.status === 'O') {
status = 'ON PROCESS';
} else if (log.status === 'P') {
status = 'PENDING';
} else if (log.status === 'R') {
status = 'ROLLBACK';
} else if (log.status === 'S') {
status = 'REVERSAL';
}
return status;
})()}
</td>
<td className="px-4 py-2 text-sm text-gray-500">{log.request_date <td className="px-4 py-2 text-sm text-gray-500">{log.request_date
? new Date(log.request_date).toLocaleDateString('en-GB', { ? new Date(log.request_date).toLocaleDateString('en-GB', {
day: '2-digit', day: '2-digit',
@ -810,9 +987,39 @@ const DetailTransaction = () => {
hour12: false hour12: false
}) })
: ''}</td> : ''}</td>
<td className="px-4 py-2 text-sm text-gray-500">{log.request_body}</td> <td className="px-4 py-2 text-sm text-gray-500">
<td className="px-4 py-2 text-sm text-gray-500">{log.response_body}</td> <pre style={{ fontFamily: 'monospace', whiteSpace: 'pre-wrap' }}>
<td className="px-4 py-2 text-sm text-gray-500">{log.request_endpoint ?? '-'}</td> {(() => {
try {
if (log.response_code === null) {
return '';
}
const parsed = JSON.parse(log.request_body);
return JSON.stringify(parsed, null, 2);
} catch (e) {
return log.request_body; // fallback: tampilkan as-is jika gagal parse
}
})()}
</pre>
</td>
<td className="px-4 py-2 text-sm text-gray-500">
<pre style={{ fontFamily: 'monospace', whiteSpace: 'pre-wrap' }}>
{(() => {
try {
if (log.response_body === null) {
return '';
}
const parsed = JSON.parse(log.response_body);
return JSON.stringify(parsed, null, 2) ?? '';
} catch (e) {
return log.response_body ?? ''; // fallback: tampilkan as-is jika gagal parse
}
})()}
</pre>
</td>
<td className="px-4 py-2 text-sm text-gray-500">{log.request_endpoint ?? ''}</td>
</tr> </tr>
)) ))
) : ( ) : (

View File

@ -178,6 +178,10 @@ const ListToolbar = () => {
<SelectItem value="R">RETURN</SelectItem> <SelectItem value="R">RETURN</SelectItem>
<SelectItem value="N">TOP UP PARTNER</SelectItem> <SelectItem value="N">TOP UP PARTNER</SelectItem>
<SelectItem value="E">REWARD</SelectItem> <SelectItem value="E">REWARD</SelectItem>
<SelectItem value="L">PURCHASE LOJA</SelectItem>
<SelectItem value="B">TOP UP P24</SelectItem>
<SelectItem value="A">TRANSFER AGENT</SelectItem>
<SelectItem value="M">WITHDRAWAL AGENT</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>

View File

@ -78,6 +78,10 @@ const TransactionProvider = ({ children }: { children: React.ReactNode }) => {
case 'R': return 'RETURN'; case 'R': return 'RETURN';
case 'N': return 'TOP UP PARTNER'; case 'N': return 'TOP UP PARTNER';
case 'E': return 'REWARD'; 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';
default: return '_'; default: return '_';
} }
}, },
@ -197,10 +201,22 @@ const TransactionProvider = ({ children }: { children: React.ReactNode }) => {
label = 'ON PROCESS'; label = 'ON PROCESS';
badgeClass = 'bg-blue-100 text-blue-800'; badgeClass = 'bg-blue-100 text-blue-800';
break; break;
default: case 'P':
label = 'PENDING'; label = 'PENDING';
badgeClass = 'bg-gray-100 text-gray-800'; badgeClass = 'bg-gray-100 text-gray-800';
break; break;
case 'R':
label = 'ROLLBACK';
badgeClass = 'bg-yellow-100 text-yellow-800';
break;
case 'S':
label = 'REVERSAL';
badgeClass = 'bg-purple-100 text-purple-800';
break;
default:
label = '';
badgeClass = 'bg-gray-100 text-gray-800';
break;
} }
return ( return (

View File

@ -90,6 +90,10 @@ const AddFeeDialog = () => {
id: customer.id, id: customer.id,
name: customer.username name: customer.username
})); }));
const [openDeductOrigin, setOpenDeductOrigin] = useState(false);
const [openCreditDestination, setOpenCreditDestination] = useState(false);
const [errors, setErrors] = useState<Record<string, string>>({}); const [errors, setErrors] = useState<Record<string, string>>({});
const initialState = { const initialState = {
name: '', name: '',
@ -101,20 +105,18 @@ const AddFeeDialog = () => {
period_end: '', period_end: '',
deduct_amount: 0, deduct_amount: 0,
deduct_percentage: 0, deduct_percentage: 0,
fee_amount: 0,
priority: '',
status: '', status: '',
status_include: '', status_include: '',
created_by: '', created_by: '',
created_at: '', created_at: '',
deduct_from: '', deduct_from: '',
deduct_origin: '00000000-0000-0000-0000-000000000000',
deduct_from_account: '', deduct_from_account: '',
credit_to: '', credit_to: '',
credit_destination: '00000000-0000-0000-0000-000000000000', credit_destination: '00000000-0000-0000-0000-000000000000',
credit_destination_account: '' credit_destination_account: ''
}; };
const [formField, setFormField] = useState(initialState); const [formField, setFormField] = useState(initialState);
const resetForm = () => { const resetForm = () => {
@ -122,6 +124,16 @@ const AddFeeDialog = () => {
setTransactionTypeName(''); setTransactionTypeName('');
}; };
const handleCloseDialog = () => {
setFormField(initialState);
setAlert({ show: false, message: '' });
setCustomerSearchTerm('');
setOpenCreditDestination(false);
setOpenDeductOrigin(false);
setOpen(false);
handleEditFeeDialog(false, null);
};
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const [showTransactionFeeDialog, setShowTransactionFeeDialog] = useState(false); const [showTransactionFeeDialog, setShowTransactionFeeDialog] = useState(false);
const parsedUser = getAuth()?.user; const parsedUser = getAuth()?.user;
@ -273,7 +285,6 @@ const AddFeeDialog = () => {
'transaction_type', 'transaction_type',
'status', 'status',
'status_include', 'status_include',
'priority',
'deduct_from', 'deduct_from',
'deduct_from_account', 'deduct_from_account',
'credit_to', 'credit_to',
@ -300,6 +311,14 @@ const AddFeeDialog = () => {
return; return;
} }
if (formField.deduct_from === 'I' && !formField.deduct_origin) {
setAlert({
show: true,
message: 'Deduct Origin is required when Input is selected'
});
setIsSubmitting(false);
return;
}
setAlert({ show: false, message: '' }); setAlert({ show: false, message: '' });
const payload = { ...formField }; const payload = { ...formField };
@ -308,6 +327,9 @@ const AddFeeDialog = () => {
payload.credit_destination = '00000000-0000-0000-0000-000000000000'; payload.credit_destination = '00000000-0000-0000-0000-000000000000';
} }
if (formField.deduct_from !== 'I') {
payload.deduct_origin = '00000000-0000-0000-0000-000000000000';
}
try { try {
const response = await PostData(`${API_URL}/transactionfees/create`, payload); const response = await PostData(`${API_URL}/transactionfees/create`, payload);
@ -368,7 +390,15 @@ const AddFeeDialog = () => {
}; };
return ( return (
<Dialog open={showAddFeeDialog} onOpenChange={(open) => handleAddFeeDialog(open)}> <Dialog
open={showAddFeeDialog}
onOpenChange={(open) => {
if (!open) {
handleAddFeeDialog(open);
handleCloseDialog();
}
}}
>
<DialogContent className="container-fixed max-w-[1080px] flex flex-col p-5 overflow-hidden [&>button]:hidden"> <DialogContent className="container-fixed max-w-[1080px] flex flex-col p-5 overflow-hidden [&>button]:hidden">
<DialogTitle></DialogTitle> <DialogTitle></DialogTitle>
<DialogDescription></DialogDescription> <DialogDescription></DialogDescription>
@ -554,30 +584,20 @@ const AddFeeDialog = () => {
placeholder="Enter Deduct Percentage" placeholder="Enter Deduct Percentage"
/> />
</div> </div>
<div className="w-full">
<label className="form-label">Fee Amount</label>
<NumericFormat
className="input"
value={formField.fee_amount}
thousandSeparator="."
decimalSeparator=","
allowNegative={false}
onValueChange={(values) => {
setFormField((prev) => ({
...prev,
fee_amount: values.floatValue || 0
}));
}}
placeholder="Enter Fee Amount"
/>
</div>
<div className="w-full"> <div className="w-full">
<label className="form-label"> <label className="form-label">
Deduct From <span className="text-red-500">*</span> Deduct From <span className="text-red-500">*</span>
</label> </label>
<Select <Select
value={formField.deduct_from} value={formField.deduct_from}
onValueChange={(value) => setFormField({ ...formField, deduct_from: value })} onValueChange={(value) =>
setFormField({
...formField,
deduct_from: value,
deduct_origin: value === 'I' ? '' : '00000000-0000-0000-0000-000000000000'
})
}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Select Deduct From" /> <SelectValue placeholder="Select Deduct From" />
@ -585,12 +605,82 @@ const AddFeeDialog = () => {
<SelectContent> <SelectContent>
<SelectItem value="D">Destination Member</SelectItem> <SelectItem value="D">Destination Member</SelectItem>
<SelectItem value="S">Source Member</SelectItem> <SelectItem value="S">Source Member</SelectItem>
<SelectItem value="I">Input</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
{formField.deduct_from === 'I' && (
<div className="w-full">
<label className="form-label">
Deduct Origin <span className="text-red-500">*</span>
</label>
<div className="relative">
<div
className="flex w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 cursor-pointer"
onClick={() => setOpenDeductOrigin(!openDeductOrigin)}
>
<span className="truncate">
{customers.find((customer) => customer.id === formField.deduct_origin)
?.username || 'Search customer...'}
</span>
</div>
{openDeductOrigin && (
<div className="absolute left-0 right-0 top-full z-50 mt-1 max-h-52 overflow-auto rounded-md border border-gray-200 bg-white shadow-lg">
<div className="sticky top-0 bg-white p-2 border-b">
<Input
className="h-8 text-sm"
type="text"
placeholder="Search customer..."
value={customerSearchTerm}
onChange={(e) => setCustomerSearchTerm(e.target.value)}
autoComplete="off"
onClick={(e) => e.stopPropagation()}
autoFocus
/>
</div>
<div>
{customers
.filter(
(customer) =>
customer.username
.toLowerCase()
.includes(customerSearchTerm.toLowerCase()) ||
customer.msisdn.includes(customerSearchTerm)
)
.map((customer) => (
<div
key={customer.id}
className="cursor-pointer px-3 py-1.5 text-sm hover:bg-gray-100"
onClick={() => {
setFormField({ ...formField, deduct_origin: customer.id });
setOpenDeductOrigin(false);
}}
>
{customer.username}
</div>
))}
{customers.filter(
(customer) =>
customer.username
.toLowerCase()
.includes(customerSearchTerm.toLowerCase()) ||
customer.msisdn.includes(customerSearchTerm)
).length === 0 && (
<div className="px-3 py-2 text-sm text-gray-500">
No customer found
</div>
)}
</div>
</div>
)}
</div>
</div>
)}
<div className="w-full"> <div className="w-full">
<label className="form-label"> <label className="form-label">
Deduct From Destination <span className="text-red-500">*</span> Deduct From Account <span className="text-red-500">*</span>
</label> </label>
{renderSelectWithLoading( {renderSelectWithLoading(
formField.deduct_from_account, formField.deduct_from_account,
@ -600,6 +690,7 @@ const AddFeeDialog = () => {
isLoadingWallets isLoadingWallets
)} )}
</div> </div>
<div className="w-full"> <div className="w-full">
<label className="form-label"> <label className="form-label">
Credit To <span className="text-red-500">*</span> Credit To <span className="text-red-500">*</span>
@ -633,17 +724,16 @@ const AddFeeDialog = () => {
<div className="relative"> <div className="relative">
<div <div
className="flex w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 cursor-pointer" className="flex w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 cursor-pointer"
onClick={() => setOpen(!open)} onClick={() => setOpenCreditDestination(!openCreditDestination)}
> >
<span className="truncate"> <span className="truncate">
{customers.find( {customers.find(
(customer) => customer.id === formField.credit_destination (customer) => customer.id === formField.credit_destination
)?.username || 'Search customer...'} )?.username || 'Search customer...'}
</span> </span>
<path d="m6 9 6 6 6-6"></path>
</div> </div>
{open && ( {openCreditDestination && (
<div className="absolute left-0 right-0 top-full z-50 mt-1 max-h-52 overflow-auto rounded-md border border-gray-200 bg-white shadow-lg"> <div className="absolute left-0 right-0 top-full z-50 mt-1 max-h-52 overflow-auto rounded-md border border-gray-200 bg-white shadow-lg">
<div className="sticky top-0 bg-white p-2 border-b"> <div className="sticky top-0 bg-white p-2 border-b">
<Input <Input
@ -675,7 +765,7 @@ const AddFeeDialog = () => {
...formField, ...formField,
credit_destination: customer.id credit_destination: customer.id
}); });
setOpen(false); setOpenCreditDestination(false);
}} }}
> >
{customer.username} {customer.username}
@ -698,6 +788,7 @@ const AddFeeDialog = () => {
</div> </div>
</div> </div>
)} )}
<div className="w-full"> <div className="w-full">
<label className="form-label"> <label className="form-label">
Credit Destination Account <span className="text-red-500">*</span> Credit Destination Account <span className="text-red-500">*</span>
@ -744,23 +835,7 @@ const AddFeeDialog = () => {
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<div className="w-full">
<label className="form-label">
Priority <span className="text-red-500">*</span>
</label>
<Select
value={formField.priority}
onValueChange={(value) => setFormField({ ...formField, priority: value })}
>
<SelectTrigger>
<SelectValue placeholder="Select Priority" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Y">Yes</SelectItem>
<SelectItem value="N">No</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex justify-end pt-2.5 gap-5"> <div className="flex justify-end pt-2.5 gap-5">
<Button <Button
variant={'outline'} variant={'outline'}

View File

@ -61,6 +61,8 @@ const EditFeeDialog = () => {
const { reload } = useDataGrid(); const { reload } = useDataGrid();
const { GetData, PutData } = useCallApi(); const { GetData, PutData } = useCallApi();
const parsedUser = getAuth()?.user; const parsedUser = getAuth()?.user;
const [openDeductOrigin, setOpenDeductOrigin] = useState(false);
const [openCreditDestination, setOpenCreditDestination] = useState(false);
const [wallets, setWallets] = useState<WalletProps[]>([]); const [wallets, setWallets] = useState<WalletProps[]>([]);
const [customers, setCustomers] = useState<CustomerProps[]>([]); const [customers, setCustomers] = useState<CustomerProps[]>([]);
@ -94,13 +96,12 @@ const EditFeeDialog = () => {
period_end: '', period_end: '',
deduct_amount: 0, deduct_amount: 0,
deduct_percentage: 0, deduct_percentage: 0,
fee_amount: 0,
priority: '',
status: '', status: '',
status_include: '', status_include: '',
updated_by: '', updated_by: '',
updated_at: '', updated_at: '',
deduct_from: '', deduct_from: '',
deduct_origin: '00000000-0000-0000-0000-000000000000',
deduct_from_account: '', deduct_from_account: '',
credit_to: '', credit_to: '',
credit_destination: '00000000-0000-0000-0000-000000000000', credit_destination: '00000000-0000-0000-0000-000000000000',
@ -143,7 +144,6 @@ const EditFeeDialog = () => {
'transaction_type', 'transaction_type',
'status', 'status',
'status_include', 'status_include',
'priority',
'deduct_from', 'deduct_from',
'deduct_from_account', 'deduct_from_account',
'credit_to', 'credit_to',
@ -170,6 +170,15 @@ const EditFeeDialog = () => {
return; return;
} }
if (formField.deduct_from === 'I' && !formField.deduct_origin) {
setAlert({
show: true,
message: 'Deduct Origin is required when Input is selected'
});
setIsSubmitting(false);
return;
}
setAlert({ show: false, message: '' }); setAlert({ show: false, message: '' });
const payload = { ...formField }; const payload = { ...formField };
@ -178,6 +187,11 @@ const EditFeeDialog = () => {
payload.credit_destination = '00000000-0000-0000-0000-000000000000'; payload.credit_destination = '00000000-0000-0000-0000-000000000000';
} }
if (formField.deduct_from !== 'I') {
payload.deduct_origin = '00000000-0000-0000-0000-000000000000';
}
// console.log(payload);
try { try {
const response = await PutData( const response = await PutData(
`${API_URL}/transactionfees/update/${selectedTransferFee}`, `${API_URL}/transactionfees/update/${selectedTransferFee}`,
@ -296,7 +310,7 @@ const EditFeeDialog = () => {
setIsLoadingTransferFee(true); setIsLoadingTransferFee(true);
try { try {
const response = await GetData(`${API_URL}/transactionfees/getdata/${id}`, {}); const response = await GetData(`${API_URL}/transactionfees/getdata/${id}`, {});
// console.log(response);
if (response?.status) { if (response?.status) {
setFormField({ setFormField({
...initialState, ...initialState,
@ -309,12 +323,12 @@ const EditFeeDialog = () => {
period_end: formatDate(response.data.period_end), period_end: formatDate(response.data.period_end),
deduct_amount: response.data.deduct_amount || 0, deduct_amount: response.data.deduct_amount || 0,
deduct_percentage: response.data.deduct_percentage || 0, deduct_percentage: response.data.deduct_percentage || 0,
fee_amount: response.data.fee_amount || 0,
priority: response.data.priority || '',
status: response.data.status || '', status: response.data.status || '',
status_include: response.data.status_include || '', status_include: response.data.status_include || '',
deduct_from: response.data.deduct_from || '', deduct_from: response.data.deduct_from || '',
deduct_from_account: response.data.deduct_from_account?.id || '', deduct_from_account: response.data.deduct_from_account?.id || '',
deduct_origin:
response.data.deduct_origin?.id || '00000000-0000-0000-0000-000000000000',
credit_to: response.data.credit_to || '', credit_to: response.data.credit_to || '',
credit_destination: credit_destination:
response.data.credit_destination?.id || '00000000-0000-0000-0000-000000000000', response.data.credit_destination?.id || '00000000-0000-0000-0000-000000000000',
@ -353,6 +367,8 @@ const EditFeeDialog = () => {
setFormField(initialState); setFormField(initialState);
setAlert({ show: false, message: '' }); setAlert({ show: false, message: '' });
setCustomerSearchTerm(''); setCustomerSearchTerm('');
setOpenCreditDestination(false);
setOpenDeductOrigin(false);
setOpen(false); setOpen(false);
handleEditFeeDialog(false, null); handleEditFeeDialog(false, null);
}; };
@ -605,31 +621,19 @@ const EditFeeDialog = () => {
/> />
</div> </div>
<div className="w-full">
<label className="form-label">Fee Amount</label>
<NumericFormat
className="input"
value={formField.fee_amount}
thousandSeparator="."
decimalSeparator=","
allowNegative={false}
onValueChange={(values) => {
setFormField((prev) => ({
...prev,
fee_amount: values.floatValue || 0
}));
}}
placeholder="Enter Fee Amount"
/>
</div>
<div className="w-full"> <div className="w-full">
<label className="form-label"> <label className="form-label">
Deduct From <span className="text-red-500">*</span> Deduct From <span className="text-red-500">*</span>
</label> </label>
<Select <Select
value={formField.deduct_from} value={formField.deduct_from}
onValueChange={(value) => setFormField({ ...formField, deduct_from: value })} onValueChange={(value) =>
setFormField({
...formField,
deduct_from: value,
deduct_origin: value === 'I' ? '' : '00000000-0000-0000-0000-000000000000'
})
}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Select Deduct From" /> <SelectValue placeholder="Select Deduct From" />
@ -637,13 +641,77 @@ const EditFeeDialog = () => {
<SelectContent> <SelectContent>
<SelectItem value="D">Destination Member</SelectItem> <SelectItem value="D">Destination Member</SelectItem>
<SelectItem value="S">Source Member</SelectItem> <SelectItem value="S">Source Member</SelectItem>
<SelectItem value="I">Input</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
{formField.deduct_from === 'I' && (
<div className="w-full">
<label className="form-label">
Deduct Origin <span className="text-red-500">*</span>
</label>
<div className="relative">
<div
className="flex w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 cursor-pointer"
onClick={() => setOpenDeductOrigin(!openDeductOrigin)}
>
<span className="truncate">
{customers.find((customer) => customer.id === formField.deduct_origin)?.username || 'Search customer...'}
</span>
</div>
{openDeductOrigin && (
<div className="absolute left-0 right-0 top-full z-50 mt-1 max-h-52 overflow-auto rounded-md border border-gray-200 bg-white shadow-lg">
<div className="sticky top-0 bg-white p-2 border-b">
<Input
className="h-8 text-sm"
type="text"
placeholder="Search customer..."
value={customerSearchTerm}
onChange={(e) => setCustomerSearchTerm(e.target.value)}
autoComplete="off"
onClick={(e) => e.stopPropagation()}
autoFocus
/>
</div>
<div>
{customers
.filter(
(customer) =>
customer.username.toLowerCase().includes(customerSearchTerm.toLowerCase()) ||
customer.msisdn.includes(customerSearchTerm)
)
.map((customer) => (
<div
key={customer.id}
className="cursor-pointer px-3 py-1.5 text-sm hover:bg-gray-100"
onClick={() => {
setFormField({ ...formField, deduct_origin: customer.id });
setOpenDeductOrigin(false);
}}
>
{customer.username}
</div>
))}
{customers.filter(
(customer) =>
customer.username.toLowerCase().includes(customerSearchTerm.toLowerCase()) ||
customer.msisdn.includes(customerSearchTerm)
).length === 0 && (
<div className="px-3 py-2 text-sm text-gray-500">No customer found</div>
)}
</div>
</div>
)}
</div>
</div>
)}
<div className="w-full"> <div className="w-full">
<label className="form-label"> <label className="form-label">
Deduct From Destination <span className="text-red-500">*</span> Deduct From Account <span className="text-red-500">*</span>
</label> </label>
{renderSelectWithLoading( {renderSelectWithLoading(
formField.deduct_from_account, formField.deduct_from_account,
@ -653,7 +721,6 @@ const EditFeeDialog = () => {
isLoadingWallets isLoadingWallets
)} )}
</div> </div>
<div className="w-full"> <div className="w-full">
<label className="form-label"> <label className="form-label">
Credit To <span className="text-red-500">*</span> Credit To <span className="text-red-500">*</span>
@ -680,79 +747,76 @@ const EditFeeDialog = () => {
</Select> </Select>
</div> </div>
{formField.credit_to === 'I' && ( {formField.credit_to === 'I' && (
<div className="w-full"> <div className="w-full">
<label className="form-label"> <label className="form-label">
Credit Destination <span className="text-red-500">*</span> Credit Destination <span className="text-red-500">*</span>
</label> </label>
<div className="relative"> <div className="relative">
<div <div
className="flex w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 cursor-pointer" className="flex w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 cursor-pointer"
onClick={() => setOpen(!open)} onClick={() => setOpenCreditDestination(!openCreditDestination)}
> >
<span className="truncate"> <span className="truncate">
{customers.find( {customers.find((customer) => customer.id === formField.credit_destination)
(customer) => customer.id === formField.credit_destination ?.username || 'Search customer...'}
)?.username || 'Search customer...'} </span>
</span> </div>
<path d="m6 9 6 6 6-6"></path>
</div> {openCreditDestination && (
<div className="absolute left-0 right-0 top-full z-50 mt-1 max-h-52 overflow-auto rounded-md border border-gray-200 bg-white shadow-lg">
<div className="sticky top-0 bg-white p-2 border-b">
<Input
className="h-8 text-sm"
type="text"
placeholder="Search customer..."
value={customerSearchTerm}
onChange={(e) => setCustomerSearchTerm(e.target.value)}
autoComplete="off"
onClick={(e) => e.stopPropagation()}
autoFocus
/>
</div>
<div>
{customers
.filter(
(customer) =>
customer.username
.toLowerCase()
.includes(customerSearchTerm.toLowerCase()) ||
customer.msisdn.includes(customerSearchTerm)
)
.map((customer) => (
<div
key={customer.id}
className="cursor-pointer px-3 py-1.5 text-sm hover:bg-gray-100"
onClick={() => {
setFormField({
...formField,
credit_destination: customer.id
});
setOpenCreditDestination(false);
}}
>
{customer.username}
</div>
))}
{customers.filter(
(customer) =>
customer.username
.toLowerCase()
.includes(customerSearchTerm.toLowerCase()) ||
customer.msisdn.includes(customerSearchTerm)
).length === 0 && (
<div className="px-3 py-2 text-sm text-gray-500">No customer found</div>
)}
</div>
</div>
)}
</div>
</div>
)}
{open && (
<div className="absolute left-0 right-0 top-full z-50 mt-1 max-h-52 overflow-auto rounded-md border border-gray-200 bg-white shadow-lg">
<div className="sticky top-0 bg-white p-2 border-b">
<Input
className="h-8 text-sm"
type="text"
placeholder="Search customer..."
value={customerSearchTerm}
onChange={(e) => setCustomerSearchTerm(e.target.value)}
autoComplete="off"
onClick={(e) => e.stopPropagation()}
autoFocus
/>
</div>
<div>
{customers
.filter(
(customer) =>
customer.username
.toLowerCase()
.includes(customerSearchTerm.toLowerCase()) ||
customer.msisdn.includes(customerSearchTerm)
)
.map((customer) => (
<div
key={customer.id}
className="cursor-pointer px-3 py-1.5 text-sm hover:bg-gray-100"
onClick={() => {
setFormField({
...formField,
credit_destination: customer.id
});
setOpen(false);
}}
>
{customer.username}
</div>
))}
{customers.filter(
(customer) =>
customer.username
.toLowerCase()
.includes(customerSearchTerm.toLowerCase()) ||
customer.msisdn.includes(customerSearchTerm)
).length === 0 && (
<div className="px-3 py-2 text-sm text-gray-500">
No customer found
</div>
)}
</div>
</div>
)}
</div>
</div>
)}
<div className="w-full"> <div className="w-full">
<label className="form-label"> <label className="form-label">
@ -805,24 +869,6 @@ const EditFeeDialog = () => {
</Select> </Select>
</div> </div>
<div className="w-full">
<label className="form-label">
Priority <span className="text-red-500">*</span>
</label>
<Select
value={formField.priority}
onValueChange={(value) => setFormField({ ...formField, priority: value })}
>
<SelectTrigger>
<SelectValue placeholder="Select Priority" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Y">Yes</SelectItem>
<SelectItem value="N">No</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex justify-end pt-2.5 gap-5"> <div className="flex justify-end pt-2.5 gap-5">
<Button variant={'outline'} type="button" onClick={resetForm}> <Button variant={'outline'} type="button" onClick={resetForm}>
Reset Reset

View File

@ -143,14 +143,6 @@ const ManageTransferFeeContextProvider = ({
enableHiding: false, enableHiding: false,
meta: { headerClassName: 'w-[150px]' } meta: { headerClassName: 'w-[150px]' }
}, },
{
accessorFn: (row) => row.fee_amount,
id: 'fee_amount',
header: ({ column }) => <DataGridColumnHeader title="Fee Amount" column={column} />,
enableSorting: true,
enableHiding: false,
meta: { headerClassName: 'w-[150px]' }
},
{ {
accessorFn: (row) => row.deduct_amount, accessorFn: (row) => row.deduct_amount,
id: 'deduct_amount', id: 'deduct_amount',
@ -235,10 +227,11 @@ const ManageTransferFeeContextProvider = ({
accessorFn: (row: { deduct_from: string }) => { accessorFn: (row: { deduct_from: string }) => {
const mapping: Record<string, string> = { const mapping: Record<string, string> = {
D: 'Destination Member', D: 'Destination Member',
S: 'Source Member' S: 'Source Member',
I: 'Input Customer'
}; };
return mapping[row.deduct_from]; return mapping[row.deduct_from] || 'Unknown';
}, },
id: 'deduct_from', id: 'deduct_from',
header: ({ column }) => <DataGridColumnHeader title="Deduct From" column={column} />, header: ({ column }) => <DataGridColumnHeader title="Deduct From" column={column} />,
@ -247,38 +240,32 @@ const ManageTransferFeeContextProvider = ({
meta: { headerClassName: 'w-[250px]' } meta: { headerClassName: 'w-[250px]' }
}, },
{ {
accessorFn: (row) => row.deduct_from_account?.description, accessorFn: (row) => {
if (row.deduct_from === 'S' || row.deduct_from === 'D' || !row.deduct_from_account) {
return 'N/A';
}
return row.deduct_from_account.description;
},
id: 'deduct_from_account', id: 'deduct_from_account',
header: ({ column }) => ( header: ({ column }) => (
<DataGridColumnHeader title="Deduct From Account" column={column} /> <DataGridColumnHeader title="Deduct From Destination" column={column} />
), ),
enableSorting: true, enableSorting: true,
enableHiding: false, enableHiding: false,
meta: { headerClassName: 'w-[250px]' } meta: { headerClassName: 'w-[250px]' }
}, },
{ {
accessorFn: (row) => row.priority, accessorFn: (row) => {
id: 'priority', if (row.deduct_from === 'S' || row.deduct_from === 'D' || !row.deduct_origin) {
header: ({ column }) => <DataGridColumnHeader title="Priority" column={column} />, return 'N/A';
}
return row.deduct_origin?.fullname;
},
id: 'deduct_origin',
header: ({ column }) => <DataGridColumnHeader title="Deduct Origin" column={column} />,
enableSorting: true, enableSorting: true,
enableHiding: false, enableHiding: false,
cell: ({ row }) => { meta: { headerClassName: 'w-[250px]' }
const isActive = row.original.priority === 'Y';
return (
<span
className={`px-2 py-1 text-xs font-semibold rounded-full ${
isActive ? 'bg-green-100 text-green-600' : 'bg-red-100 text-red-600'
}`}
>
{isActive ? 'Yes' : 'No'}
</span>
);
},
meta: {
headerClassName: 'w-[100px]',
cellClassName: 'text-center'
}
}, },
{ {
accessorFn: (row) => row.status, accessorFn: (row) => row.status,

View File

@ -378,6 +378,10 @@ const AddDialog = () => {
thousandSeparator="." thousandSeparator="."
decimalSeparator="," decimalSeparator=","
allowNegative={false} allowNegative={false}
decimalScale={0}
isAllowed={({ floatValue }) =>
floatValue === undefined || Number.isInteger(floatValue)
}
onValueChange={(values) => { onValueChange={(values) => {
setFormField((prev) => ({ setFormField((prev) => ({
...prev, ...prev,
@ -561,6 +565,9 @@ const AddDialog = () => {
<SelectItem value="N">Top Up Patner</SelectItem> <SelectItem value="N">Top Up Patner</SelectItem>
<SelectItem value="E">Reward</SelectItem> <SelectItem value="E">Reward</SelectItem>
<SelectItem value="L">Purchase Loja</SelectItem> <SelectItem value="L">Purchase Loja</SelectItem>
<SelectItem value="B">Top Up P24</SelectItem>
<SelectItem value="A">Transfer Agent</SelectItem>
<SelectItem value="M">Withdraw Agent</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
{errors.status_kind && ( {errors.status_kind && (

View File

@ -574,6 +574,10 @@ const EditDialog = () => {
thousandSeparator="." thousandSeparator="."
decimalSeparator="," decimalSeparator=","
allowNegative={false} allowNegative={false}
decimalScale={0}
isAllowed={({ floatValue }) =>
floatValue === undefined || Number.isInteger(floatValue)
}
onValueChange={(values) => { onValueChange={(values) => {
setFormField((prev) => ({ setFormField((prev) => ({
...prev, ...prev,
@ -759,6 +763,9 @@ const EditDialog = () => {
<SelectItem value="N">Top Up Patner</SelectItem> <SelectItem value="N">Top Up Patner</SelectItem>
<SelectItem value="E">Reward</SelectItem> <SelectItem value="E">Reward</SelectItem>
<SelectItem value="L">Purchase Loja</SelectItem> <SelectItem value="L">Purchase Loja</SelectItem>
<SelectItem value="B">Top Up P24</SelectItem>
<SelectItem value="A">Transfer Agent</SelectItem>
<SelectItem value="M">Withdraw Agent</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
{errors.status_kind && ( {errors.status_kind && (

View File

@ -207,6 +207,9 @@ const ListToolbar = () => {
<SelectItem value="N">Top Up Patner</SelectItem> <SelectItem value="N">Top Up Patner</SelectItem>
<SelectItem value="E">Reward</SelectItem> <SelectItem value="E">Reward</SelectItem>
<SelectItem value="L">Purchase Loja</SelectItem> <SelectItem value="L">Purchase Loja</SelectItem>
<SelectItem value="B">Top Up P24</SelectItem>
<SelectItem value="A">Transfer Agent</SelectItem>
<SelectItem value="M">Withdraw Agent</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>

View File

@ -24,13 +24,19 @@ function useDebounce<T>(value: T, delay: number): T {
const formatNumber = (num: number): string => { const formatNumber = (num: number): string => {
return num.toLocaleString('en-US', { return num.toLocaleString('en-US', {
style: 'currency', style: 'decimal',
currency: 'USD',
minimumFractionDigits: 2, minimumFractionDigits: 2,
maximumFractionDigits: 2 maximumFractionDigits: 2
}); });
}; };
const formatInteger = (num: number): string => {
return num.toLocaleString('en-US', {
style: 'decimal',
maximumFractionDigits: 0
})
}
interface AccountProps { interface AccountProps {
id: string; id: string;
name: string; name: string;
@ -195,7 +201,7 @@ const ManageTransferTypeContextProvider = ({ children }: { children: React.React
header: ({ column }) => ( header: ({ column }) => (
<DataGridColumnHeader title="Max Transaction Per Day" column={column} /> <DataGridColumnHeader title="Max Transaction Per Day" column={column} />
), ),
cell: ({ row }) => formatNumber(row.original.max_transaction_per_day), cell: ({ row }) => formatInteger(row.original.max_transaction_per_day),
enableSorting: false, enableSorting: false,
enableHiding: false, enableHiding: false,
meta: { headerClassName: 'w-[250px]' } meta: { headerClassName: 'w-[250px]' }
@ -278,7 +284,10 @@ const ManageTransferTypeContextProvider = ({ children }: { children: React.React
U: { label: 'Top Up', className: 'bg-yellow-100 text-yellow-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' }, E: { label: 'Reward', className: 'bg-rose-100 text-rose-600' },
L: { label: 'Purchase Loja', className: 'bg-rose-100 text-rose-600' } L: { label: 'Purchase Loja', className: 'bg-rose-100 text-rose-600' },
B: { label: 'Top Up P24', className: 'bg-rose-100 text-rose-600' },
A: { label: 'Transfer Agent', className: 'bg-rose-100 text-rose-600' },
M: { label: 'Withdraw Agent', className: 'bg-rose-100 text-rose-600' }
}; };
const kindInfo = mapping[kind] || { const kindInfo = mapping[kind] || {
@ -371,12 +380,11 @@ const ManageTransferTypeContextProvider = ({ children }: { children: React.React
filterObject['wallet_destination.name'] = filter.value; filterObject['wallet_destination.name'] = filter.value;
} else if (filter.id === 'status_kind') { } else if (filter.id === 'status_kind') {
filterObject[filter.id] = filter.value; filterObject[filter.id] = filter.value;
} else if(filter.id === 'status_approval'){ } else if (filter.id === 'status_approval') {
filterObject[filter.id] = filter.value; filterObject[filter.id] = filter.value;
} else if (filter.id === 'type') { } else if (filter.id === 'type') {
filterObject[filter.id] = filter.value; filterObject[filter.id] = filter.value;
} } else {
else {
filterObject[filter.id] = filter.value.toLowerCase(); filterObject[filter.id] = filter.value.toLowerCase();
} }
} }