This commit is contained in:
unknown
2025-05-06 17:10:00 +07:00
19 changed files with 588 additions and 314 deletions

Binary file not shown.

View File

@ -150,7 +150,7 @@ const Login = () => {
/> />
<span className="checkbox-label">Remember me</span> <span className="checkbox-label">Remember me</span>
</label> </label>
<Link {/* <Link
to={ to={
currentLayout?.name === 'auth-branded' currentLayout?.name === 'auth-branded'
? '/auth/reset-password' ? '/auth/reset-password'
@ -159,7 +159,7 @@ const Login = () => {
className="text-2sm link shrink-0" className="text-2sm link shrink-0"
> >
Forgot Password? Forgot Password?
</Link> </Link> */}
</div> </div>
<button <button

View File

@ -53,7 +53,7 @@ const MemberActivity = ({ startdate, enddate }: Props) => {
const arcY = 50 - radius * Math.sin(arcAngle); const arcY = 50 - radius * Math.sin(arcAngle);
return ( return (
<div className="p-6 bg-white rounded-lg shadow-md w-full max-w-xl"> <div className="p-6 bg-white rounded-lg shadow-md w-1/3">
<div className="flex justify-between items-center pb-3 mb-4"> <div className="flex justify-between items-center 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>

View File

@ -46,7 +46,7 @@ const TransactionPieChart = ({ startdate, enddate }: Props) => {
]; ];
return ( return (
<div className="bg-white rounded-lg shadow-md p-6 w-full max-w-md"> <div className="bg-white rounded-lg shadow-md p-6 w-1/3">
<div className="flex justify-between items-center mb-4"> <div className="flex justify-between items-center mb-4">
<h2 className="text-lg font-semibold text-gray-700">Transaction Chart</h2> <h2 className="text-lg font-semibold text-gray-700">Transaction Chart</h2>
</div> </div>

View File

@ -80,7 +80,7 @@ const TransactionValue = ({ startdate, enddate }: Props) => {
const maxValue = transactionData.length > 0 ? Math.max(...transactionData.map(item => item.value)) : 1; const maxValue = transactionData.length > 0 ? Math.max(...transactionData.map(item => item.value)) : 1;
return ( return (
<div className="bg-white rounded-xl shadow p-6 w-full max-w-md"> <div className="bg-white rounded-xl shadow p-6 w-1/3">
<div className="flex justify-between items-center mb-4"> <div className="flex justify-between items-center mb-4">
<h2 className="text-lg font-semibold text-gray-800">Transaction Value</h2> <h2 className="text-lg font-semibold text-gray-800">Transaction Value</h2>
</div> </div>

View File

@ -3,6 +3,7 @@ import { useTransactionContext } from '../hooks/useTransactionContext';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { useCallback, useState, useEffect } from 'react'; import { useCallback, useState, useEffect } from 'react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { toAbsoluteUrl } from '@/utils';
const ListToolbar = () => { const ListToolbar = () => {
const { table, reload } = useDataGrid(); const { table, reload } = useDataGrid();
@ -23,8 +24,8 @@ const ListToolbar = () => {
nextWeek.setDate(today.getDate() + 7); nextWeek.setDate(today.getDate() + 7);
settrxDate({ settrxDate({
from: formatDate(today), // Set 'from' to today from: formatDate(today), // Set 'from' to today
to: formatDate(nextWeek), // Set 'to' to 7 days later to: formatDate(nextWeek) // Set 'to' to 7 days later
}); });
}, []); }, []);
@ -37,6 +38,15 @@ const ListToolbar = () => {
} }
}, [trxDate, table]); }, [trxDate, table]);
const handleExportData = () => {
const link = document.createElement('a');
link.href = toAbsoluteUrl('/media/file-templates/batch-sample.xlsx');
link.download = 'upload-batch-template.xlsx';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
useEffect(() => { useEffect(() => {
if (trxDate.from && trxDate.to) { if (trxDate.from && trxDate.to) {
handleFilterData(); handleFilterData();
@ -55,6 +65,15 @@ const ListToolbar = () => {
> >
Upload Batch Upload Batch
</Button> </Button>
<DefaultTooltip title={'Download Template'} placement={'top'}>
<Button onClick={handleExportData} className="h-7.5" variant={'outline'}>
<img
src={toAbsoluteUrl('/media/file-types/xls.svg')}
className="dark:hidden h-5"
alt="Export to Excel"
/>
</Button>
</DefaultTooltip>
<DefaultTooltip title={'Refresh'} placement={'top'}> <DefaultTooltip title={'Refresh'} placement={'top'}>
<Button variant="outline" className="h-7.5" onClick={() => reload()}> <Button variant="outline" className="h-7.5" onClick={() => reload()}>
<KeenIcon icon="arrows-circle" /> <KeenIcon icon="arrows-circle" />

View File

@ -24,6 +24,7 @@ import { toast } from 'sonner';
import { useCallApi } from '@/hooks'; import { useCallApi } from '@/hooks';
import { doSaveLogActivity } from '@/actions/GlobalActions'; import { doSaveLogActivity } from '@/actions/GlobalActions';
import clsx from 'clsx'; import clsx from 'clsx';
import { RefreshCw } from 'lucide-react';
const API_URL = apiConfig.service_disbursement; const API_URL = apiConfig.service_disbursement;
@ -32,17 +33,18 @@ const UploadBatchDialog = () => {
const { showUploadBatchDialog, handleUploadBatchDialog } = useTransactionContext(); const { showUploadBatchDialog, handleUploadBatchDialog } = useTransactionContext();
const { reload } = useDataGrid(); const { reload } = useDataGrid();
const { PostData, PostDataFile, GetData } = useCallApi(); const { PostData, PostDataFile, GetData } = useCallApi();
const [isSubmitting, setIsSubmitting] = useState(false);
const [alert, setAlert] = useState({ const [alert, setAlert] = useState({
show: false, show: false,
message: '' message: ''
}); });
const initialState: { const initialState: {
execution_date: string, execution_date: string;
file: File | null; file: File | null;
} = { } = {
execution_date: '', execution_date: '',
file: null file: null
} };
const [formField, setFormField] = useState(initialState); const [formField, setFormField] = useState(initialState);
const resetForm = () => { const resetForm = () => {
@ -54,9 +56,11 @@ const UploadBatchDialog = () => {
const doUploadBatch = useCallback( const doUploadBatch = useCallback(
async (e: React.FormEvent<HTMLFormElement>) => { async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
setIsSubmitting(true);
const formData = new FormData(); const formData = new FormData();
formData.append('execution_date', formField.execution_date); formData.append('execution_date', formField.execution_date);
if (formField.file) { if (formField.file) {
formData.append('file', formField.file); formData.append('file', formField.file);
} }
@ -88,6 +92,8 @@ const UploadBatchDialog = () => {
} catch (error) { } catch (error) {
toast.error('Error uploading batch'); toast.error('Error uploading batch');
setAlert({ show: true, message: 'Something went wrong. Please try again.' }); setAlert({ show: true, message: 'Something went wrong. Please try again.' });
} finally {
setIsSubmitting(false);
} }
}, },
[formField] [formField]
@ -98,10 +104,7 @@ const UploadBatchDialog = () => {
console.log('Form data before submit:', formField); console.log('Form data before submit:', formField);
if ( if (formField.execution_date.trim() === '' || formField.file === null) {
formField.execution_date.trim() === '' ||
formField.file === null
) {
setAlert({ show: true, message: 'Please fill in all required fields.' }); setAlert({ show: true, message: 'Please fill in all required fields.' });
return; return;
} }
@ -150,7 +153,9 @@ const UploadBatchDialog = () => {
<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">
<label className="form-label flex items-center gap-1 max-w-56">Execution Date</label> <label className="form-label flex items-center gap-1 max-w-56">
Execution Date
</label>
<Input <Input
className="input" className="input"
type="datetime-local" type="datetime-local"
@ -167,19 +172,23 @@ const UploadBatchDialog = () => {
<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">File</label> <label className="form-label flex items-center gap-1 max-w-56">File</label>
<Input <Input
className="input" className="input"
type="file" type="file"
autoComplete="off" autoComplete="off"
required required
onChange={({ target }) => onChange={({ target }) =>
setFormField((prev) => ({ ...prev, file: target.files?.[0] ?? null })) setFormField((prev) => ({ ...prev, file: target.files?.[0] ?? null }))
} }
/> />
</div> </div>
</div> </div>
<div className="flex justify-end pt-2.5"> <div className="flex justify-end pt-2.5">
<Button className="btn btn-primary" type="submit"> <Button variant="default" type="submit" disabled={isSubmitting}>
Save Changes {isSubmitting ? (
<RefreshCw className="animate-spin h-8 w-8 text-white mx-3" />
) : (
'Submit'
)}
</Button> </Button>
</div> </div>
</div> </div>

View File

@ -1,5 +1,5 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { Dialog, DialogContent } from '@/components/ui/dialog'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { import {
Select, Select,
SelectContent, SelectContent,
@ -59,16 +59,12 @@ const EditDialog = () => {
if (response?.status) { if (response?.status) {
toast.success('Feedback reviewed successfully'); toast.success('Feedback reviewed successfully');
// Close the edit dialog
handleEditDialog(false, null); handleEditDialog(false, null);
// First close the detail dialog to reset its state
handleDetailDialog(false, null); handleDetailDialog(false, null);
// Refresh the main data grid first
refreshData(); refreshData();
// Wait a tiny bit then reopen the detail with refreshed data
setTimeout(() => { setTimeout(() => {
handleDetailDialog(true, selectedFeedback); handleDetailDialog(true, selectedFeedback);
}, 300); }, 300);
@ -97,7 +93,6 @@ const EditDialog = () => {
} }
const data = response.data; const data = response.data;
// console.log('Fetched data:', data);
setReviewNotes(data.review_notes || ''); setReviewNotes(data.review_notes || '');
@ -112,7 +107,11 @@ const EditDialog = () => {
return ( return (
<Dialog open={showEditDialog} onOpenChange={(open) => handleEditDialog(open, null)}> <Dialog open={showEditDialog} onOpenChange={(open) => handleEditDialog(open, null)}>
<DialogContent className="p-0 w-full sm:max-w-2xl bg-white rounded-lg overflow-hidden"> <DialogContent className="p-0 w-full sm:max-w-2xl bg-white rounded-lg overflow-hidden [&>button]:hidden">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<div className="relative"> <div className="relative">
<div className="p-6 flex items-center justify-between border-b"> <div className="p-6 flex items-center justify-between border-b">
<div> <div>

View File

@ -65,7 +65,7 @@ const DetailMember = ({ showAddDialog, setShowAddDialog, handleSubmit, initialDa
const handleChange = async (e: any) => { const handleChange = async (e: any) => {
const { name, value } = e.target; const { name, value } = e.target;
if (name === "file_selfie" || name === "photouser" || name === 'file_document_id' || name === "file_document_id_selfie" || if (name === "file_selfie" || name === "photouser" || name === 'file_document_id' || name === "file_document_id_selfie" ||
name === "file_commercial_license" || name === 'photomerchant') { // FOR FILE ONLY name === "file_commercial_license") { // FOR FILE ONLY
setFormData({ ...formData, [name]: e.target.files[0] }); setFormData({ ...formData, [name]: e.target.files[0] });
} else if(name === "nationality") { } else if(name === "nationality") {
setFormData({ ...formData, [name]: value }); setFormData({ ...formData, [name]: value });
@ -258,7 +258,6 @@ const DetailMember = ({ showAddDialog, setShowAddDialog, handleSubmit, initialDa
{(formData.id || dialogType === "create") ? generateInput(formData, handleChange, 'Merchant Address', 'merchantaddress', 'text', false, viewOnly): ''} {(formData.id || dialogType === "create") ? generateInput(formData, handleChange, 'Merchant Address', 'merchantaddress', 'text', false, viewOnly): ''}
{(formData.id || dialogType === "create") ? generateInput(formData, handleChange, 'Longitude Merchant', 'longitudemerchant', 'text', false, viewOnly): ''} {(formData.id || dialogType === "create") ? generateInput(formData, handleChange, 'Longitude Merchant', 'longitudemerchant', 'text', false, viewOnly): ''}
{(formData.id || dialogType === "create") ? generateInput(formData, handleChange, 'Latitude Merchant', 'latitudemerchant', 'text', false, viewOnly): ''} {(formData.id || dialogType === "create") ? generateInput(formData, handleChange, 'Latitude Merchant', 'latitudemerchant', 'text', false, viewOnly): ''}
{(formData.id || dialogType === "create") ? generateImage(formData, handleChange, 'Photo Merchant', 'photomerchant', previewImg, setPreviewImg): ''}
{(formData.id || dialogType === "create") ? generateImage(formData, handleChange, 'File Selfie', 'file_selfie', previewImg, setPreviewImg): ''} {(formData.id || dialogType === "create") ? generateImage(formData, handleChange, 'File Selfie', 'file_selfie', previewImg, setPreviewImg): ''}
{(formData.id || dialogType === "create") ? generateImage(formData, handleChange, 'File Document', 'file_document_id', previewImg, setPreviewImg): ''} {(formData.id || dialogType === "create") ? generateImage(formData, handleChange, 'File Document', 'file_document_id', previewImg, setPreviewImg): ''}
{(formData.id || dialogType === "create") ? generateImage(formData, handleChange, 'File Document & Selfie', 'file_document_id_selfie', previewImg, setPreviewImg): ''} {(formData.id || dialogType === "create") ? generateImage(formData, handleChange, 'File Document & Selfie', 'file_document_id_selfie', previewImg, setPreviewImg): ''}

View File

@ -10,8 +10,10 @@ import {
DialogHeader, DialogHeader,
DialogTitle DialogTitle
} from '@/components/ui/dialog'; } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
const API_URL = apiConfig.transaction; const API_URL = apiConfig.transaction;
const API_URL_CURRENT_BALANCE = apiConfig.service_wallet;
const DetailApprovalTransaction = () => { const DetailApprovalTransaction = () => {
const { GetData } = useCallApi(); const { GetData } = useCallApi();
@ -51,6 +53,60 @@ const DetailApprovalTransaction = () => {
} }
}, [showDetailDialog]); }, [showDetailDialog]);
const [currentBalance, setCurrentBalance] = useState<number | null>(null);
const [currentBalanceTransfer, setCurrentBalanceTransfer] = useState<number | null>(null);
useEffect(() => {
setCurrentBalance(null);
}, [transactionDetails]);
useEffect(() => {
setCurrentBalanceTransfer(null);
}, [transactionDetails]);
const getCurrentBallanceWallet = async (customerId: string) => {
try {
const responsecurrentbalance = await GetData(
`${API_URL_CURRENT_BALANCE}/dashboard/balance/account/${customerId}`,
{}
);
const match = responsecurrentbalance?.data?.find(
(item: any) => item.id_wallet === transactionDetails?.origin_wallet?.id
);
if (match) {
// console.log('Matched Wallet:', match); // Optional debug
return match.amount;
}
} catch (error) {
console.error('Error fetching current balance', error);
}
return null;
};
const getCurrentBallanceWalletTransfer = async (customerIdtransfer: string) => {
try {
const response = await GetData(
`${API_URL_CURRENT_BALANCE}/dashboard/balance/account/${customerIdtransfer}`,
{}
);
const match = response?.data?.find(
(item: any) => item.id_wallet === transactionDetails?.transfer?.destination_wallet?.id
);
if (match) {
// console.log('Matched Wallet:', match);
return match.amount;
}
} catch (error) {
console.error('Error fetching current balance', error);
}
return null;
};
return ( return (
<Dialog open={showDetailDialog} onOpenChange={setShowDetailDialog}> <Dialog open={showDetailDialog} onOpenChange={setShowDetailDialog}>
@ -146,14 +202,20 @@ const DetailApprovalTransaction = () => {
<div> <div>
<p className="text-sm text-gray-500">Amount</p> <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) ?? new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(transactionDetails?.transfer.amount)}</p> <p className="font-medium">
{(transactionDetails?.purchase?.amount ?? transactionDetails?.transfer?.amount) != null
? new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(
transactionDetails.purchase?.amount ?? transactionDetails.transfer?.amount
)
: "-"}
</p>
</div> </div>
<div> <div>
<p className="text-sm text-gray-500">Fee</p> <p className="text-sm text-gray-500">Fee</p>
<p className="font-medium"> <p className="font-medium">
{transactionDetails?.kind === 'P' {(transactionDetails?.transfer?.fee_amount ?? transactionDetails?.purchase?.fee_amount ?? 0)
? transactionDetails?.purchase.fee_amount.toLocaleString('en-US', { style: 'currency', currency: 'USD' }) .toLocaleString('en-US', { style: 'currency', currency: 'USD' })}
: transactionDetails?.transfer.fee_amount.toLocaleString('en-US', { style: 'currency', currency: 'USD' })}
</p> </p>
</div> </div>
<div> <div>
@ -234,19 +296,33 @@ const DetailApprovalTransaction = () => {
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<p className="text-sm text-gray-500">Amount</p> <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)}</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>
<div> <div>
<p className="text-sm text-gray-500">Cashback</p> <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)}</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>
<div> <div>
<p className="text-sm text-gray-500">Cashback Point</p> <p className="text-sm text-gray-500">Cashback Point</p>
<p className="font-medium">{transactionDetails?.purchase.cashback_point}</p> <p className="font-medium">
{transactionDetails?.purchase?.cashback_point ?? transactionDetails?.transfer?.cashback_point ?? 0}
</p>
</div> </div>
<div> <div>
<p className="text-sm text-gray-500">Fee Amount</p> <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)}</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>
</div> </div>
@ -471,7 +547,27 @@ const DetailApprovalTransaction = () => {
<p className="text-sm text-gray-500">Description</p> <p className="text-sm text-gray-500">Description</p>
<p className="font-medium">{transactionDetails?.origin_wallet.description}</p> <p className="font-medium">{transactionDetails?.origin_wallet.description}</p>
</div> </div>
<div>
<p className="text-sm text-gray-500">Current Balance</p>
<p className="font-medium">
{currentBalance !== null && currentBalance !== undefined
? new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(currentBalance)
: '-'}
</p>
</div>
</div> </div>
<Button
className="btn btn-primary"
type="button"
onClick={async () => {
const balance = await getCurrentBallanceWallet(transactionDetails?.origin_customer.id);
if (balance !== null) {
setCurrentBalance(balance);
}
}}
>
Generate Current Balance
</Button>
</div> </div>
)} )}
@ -482,16 +578,42 @@ const DetailApprovalTransaction = () => {
{!transactionDetails?.transfer ? ( {!transactionDetails?.transfer ? (
<div className="text-center text-sm text-gray-500">No Data available</div> <div className="text-center text-sm text-gray-500">No Data available</div>
) : ( ) : (
<div className="grid grid-cols-2 gap-4"> <>
<div> <div className="grid grid-cols-2 gap-4">
<p className="text-sm text-gray-500">Name</p> <div>
<p className="font-medium">{transactionDetails.transfer.destination_wallet.name}</p> <p className="text-sm text-gray-500">Name</p>
<p className="font-medium">{transactionDetails.transfer.destination_wallet.name}</p>
</div>
<div>
<p className="text-sm text-gray-500">Description</p>
<p className="font-medium">{transactionDetails.transfer.destination_wallet.description}</p>
</div>
<div>
<p className="text-sm text-gray-500">Current Balance</p>
<p className="font-medium">
{currentBalanceTransfer !== null
? new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(currentBalanceTransfer)
: '-'}
</p>
</div>
</div> </div>
<div> <Button
<p className="text-sm text-gray-500">Description</p> className="btn btn-primary"
<p className="font-medium">{transactionDetails.transfer.destination_wallet.description}</p> type="button"
</div> onClick={async () => {
</div> const customerId = transactionDetails?.transfer?.destination_customer?.id;
const balance = await getCurrentBallanceWalletTransfer(customerId);
if (balance !== null) {
setCurrentBalanceTransfer(balance); // ✅ INI YANG BENAR
}
}}
>
Generate Current Balance
</Button>
</>
)} )}
</div> </div>
)} )}
@ -540,8 +662,8 @@ const DetailApprovalTransaction = () => {
}) })
: ''}</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">{log.request_body}</td>
<td className="px-4 py-2 text-sm text-gray-500">{log.response_body}</td> <td className="px-4 py-2 text-sm text-gray-500">{log.response_body}</td>
<td className="px-4 py-2 text-sm text-gray-500">{log.request_endpoint}</td> <td className="px-4 py-2 text-sm text-gray-500">{log.request_endpoint}</td>
</tr> </tr>
)) ))
) : ( ) : (

View File

@ -40,9 +40,9 @@ const ListToolbar = () => {
// useEffect to set the default date values // useEffect to set the default date values
useEffect(() => { useEffect(() => {
const today = new Date(); const today = new Date();
const firstDayOfMonth = new Date(today.getFullYear(), today.getMonth(), 1); // const firstDayOfMonth = new Date(today.getFullYear(), today.getMonth(), 1);
settrxDate({ settrxDate({
from: formatDate(firstDayOfMonth), from: formatDate(today),
to: formatDate(today), to: formatDate(today),
}); });
}, []); }, []);
@ -121,7 +121,7 @@ const ListToolbar = () => {
<KeenIcon icon="magnifier" /> <KeenIcon icon="magnifier" />
<input <input
type="text" type="text"
placeholder="Search Transaction ID" placeholder="Search Transaction ID, MSISDN, Or Full Name Customer"
value={searchValue} value={searchValue}
onChange={(event) => setSearchValue(event.target.value)} onChange={(event) => setSearchValue(event.target.value)}
/> />
@ -136,13 +136,13 @@ const ListToolbar = () => {
className="h-7.5" className="h-7.5"
onClick={() => { onClick={() => {
const today = new Date(); const today = new Date();
const firstDayOfMonth = new Date(today.getFullYear(), today.getMonth(), 1); // const firstDayOfMonth = new Date(today.getFullYear(), today.getMonth(), 1);
// Resetting all filters // Resetting all filters
setSearchValue(''); setSearchValue('');
setStatusApproval(''); setStatusApproval('');
settrxDate({ settrxDate({
from: formatDate(firstDayOfMonth), from: formatDate(today),
to: formatDate(today), to: formatDate(today),
}); });

View File

@ -270,16 +270,19 @@ const ApprovalTransactionProvider = ({ children }: { children: React.ReactNode }
}); });
} }
// Constructing filter object with dynamic properties
const formattedFilter: any = { const formattedFilter: any = {
"Transactions.transaction_date": { "Transactions.transaction_date": {
from: `${startdate} 00:00:00`, from: `${startdate} 00:00:00`,
to: `${enddate} 23:59:59`, to: `${enddate} 23:59:59`
}, }
}; };
if (transactioncode) { if (transactioncode) {
formattedFilter["Transactions.code"] = { like: `%${transactioncode}%` }; formattedFilter["$or"] = [
{ "Transactions.code": { like: `%${transactioncode}%` } },
{ "Transactions.msisdn": { like: `${transactioncode}` } },
{ "origin_customer.fullname": { like: `${transactioncode}` } }
];
} }
// Handle approval status filter // Handle approval status filter

View File

@ -13,11 +13,15 @@ import {
DialogTitle DialogTitle
} from '@/components/ui/dialog'; } from '@/components/ui/dialog';
import { useDataGrid } from '@/components'; import { useDataGrid } from '@/components';
import { Button } from '@/components/ui/button';
const API_URL = apiConfig.transaction; const API_URL = apiConfig.transaction;
const API_URL_CURRENT_BALANCE = apiConfig.service_wallet;
const DetailTransaction = () => { const DetailTransaction = () => {
const { GetData } = useCallApi(); const { GetData } = useCallApi();
// const { reload } = useDataGrid(); // const { reload } = useDataGrid();
const { const {
showDetailDialog, showDetailDialog,
@ -67,6 +71,61 @@ const DetailTransaction = () => {
} }
}, [showDetailDialog]); }, [showDetailDialog]);
const [currentBalance, setCurrentBalance] = useState<number | null>(null);
const [currentBalanceTransfer, setCurrentBalanceTransfer] = useState<number | null>(null);
useEffect(() => {
setCurrentBalance(null);
}, [transactionDetails]);
useEffect(() => {
setCurrentBalanceTransfer(null);
}, [transactionDetails]);
const getCurrentBallanceWallet = async (customerId: string) => {
try {
const responsecurrentbalance = await GetData(
`${API_URL_CURRENT_BALANCE}/dashboard/balance/account/${customerId}`,
{}
);
const match = responsecurrentbalance?.data?.find(
(item: any) => item.id_wallet === transactionDetails?.origin_wallet?.id
);
if (match) {
// console.log('Matched Wallet:', match); // Optional debug
return match.amount;
}
} catch (error) {
console.error('Error fetching current balance', error);
}
return null;
};
const getCurrentBallanceWalletTransfer = async (customerIdtransfer: string) => {
try {
const response = await GetData(
`${API_URL_CURRENT_BALANCE}/dashboard/balance/account/${customerIdtransfer}`,
{}
);
const match = response?.data?.find(
(item: any) => item.id_wallet === transactionDetails?.transfer?.destination_wallet?.id
);
if (match) {
// console.log('Matched Wallet:', match);
return match.amount;
}
} catch (error) {
console.error('Error fetching current balance', error);
}
return null;
};
return ( return (
<Dialog open={showDetailDialog} onOpenChange={setShowDetailDialog}> <Dialog open={showDetailDialog} onOpenChange={setShowDetailDialog}>
@ -162,14 +221,20 @@ const DetailTransaction = () => {
</div> </div>
<div> <div>
<p className="text-sm text-gray-500">Amount</p> <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) ?? new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(transactionDetails?.transfer.amount)}</p> <p className="font-medium">
{(transactionDetails?.purchase?.amount ?? transactionDetails?.transfer?.amount) != null
? new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(
transactionDetails.purchase?.amount ?? transactionDetails.transfer?.amount
)
: "-"}
</p>
</div> </div>
<div> <div>
<p className="text-sm text-gray-500">Fee</p> <p className="text-sm text-gray-500">Fee</p>
<p className="font-medium"> <p className="font-medium">
{transactionDetails?.kind === 'P' {(transactionDetails?.transfer?.fee_amount ?? transactionDetails?.purchase?.fee_amount ?? 0)
? transactionDetails?.purchase.fee_amount.toLocaleString('en-US', { style: 'currency', currency: 'USD' }) .toLocaleString('en-US', { style: 'currency', currency: 'USD' })}
: transactionDetails?.transfer.fee_amount.toLocaleString('en-US', { style: 'currency', currency: 'USD' })}
</p> </p>
</div> </div>
<div> <div>
@ -231,16 +296,6 @@ const DetailTransaction = () => {
</div> </div>
</div> </div>
<h3 className="font-semibold flex items-center">
Origin Wallet
<span className="ml-2 bg-blue-100 text-blue-800 text-xs font-semibold mr-2 px-2.5 py-0.5 rounded">
Wallet
</span>
</h3>
<div className="grid grid-cols-2 gap-4">
</div>
<h3 className="font-semibold flex items-center"> <h3 className="font-semibold flex items-center">
Purchase Purchase
<span className="ml-2 bg-blue-100 text-blue-800 text-xs font-semibold mr-2 px-2.5 py-0.5 rounded"> <span className="ml-2 bg-blue-100 text-blue-800 text-xs font-semibold mr-2 px-2.5 py-0.5 rounded">
@ -250,69 +305,91 @@ const DetailTransaction = () => {
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<p className="text-sm text-gray-500">Amount</p> <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)}</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>
<div> <div>
<p className="text-sm text-gray-500">Cashback</p> <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)}</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>
<div> <div>
<p className="text-sm text-gray-500">Cashback Point</p> <p className="text-sm text-gray-500">Cashback Point</p>
<p className="font-medium">{transactionDetails?.purchase.cashback_point}</p> <p className="font-medium">
{transactionDetails?.purchase?.cashback_point ?? transactionDetails?.transfer?.cashback_point ?? 0}
</p>
</div> </div>
<div> <div>
<p className="text-sm text-gray-500">Fee Amount</p> <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)}</p>
</div>
</div>
</div>
)}
{activeTab === 'detail' && transactionDetails?.kind === 'P' && (
<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>
{new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(transactionDetails?.purchase.product.price_cash)}
</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"> <p className="font-medium">
{(() => { {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(
let providertype; transactionDetails?.purchase?.fee_amount ?? transactionDetails?.transfer?.fee_amount ?? 0
if (transactionDetails?.purchase.product.provider.type === 'h2h') { )}
providertype = 'HOST TO HOST';
} else if (transactionDetails?.purchase.product.provider.type === 'agent') {
providertype = 'AGENT';
}
return providertype;
})()}
</p> </p>
</div> </div>
</div> </div>
</div> </div>
)} )}
{/* {activeTab === 'detail' && transactionDetails?.kind === 'P' && ( */}
<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">
@ -348,7 +425,7 @@ const DetailTransaction = () => {
<div> <div>
<p className="text-sm text-gray-500">Fee</p> <p className="text-sm text-gray-500">Fee</p>
<p className="font-medium"> <p className="font-medium">
{transactionDetails?.transfer.fee_amount.toLocaleString('en-US', { style: 'currency', currency: 'USD' })} {(transactionDetails?.transfer?.fee_amount ?? transactionDetails?.purchase?.fee_amount ?? 0).toLocaleString('en-US', { style: 'currency', currency: 'USD' })}
</p> </p>
</div> </div>
<div> <div>
@ -482,7 +559,27 @@ const DetailTransaction = () => {
<p className="text-sm text-gray-500">Description</p> <p className="text-sm text-gray-500">Description</p>
<p className="font-medium">{transactionDetails?.origin_wallet.description}</p> <p className="font-medium">{transactionDetails?.origin_wallet.description}</p>
</div> </div>
<div>
<p className="text-sm text-gray-500">Current Balance</p>
<p className="font-medium">
{currentBalance !== null && currentBalance !== undefined
? new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(currentBalance)
: '-'}
</p>
</div>
</div> </div>
<Button
className="btn btn-primary"
type="button"
onClick={async () => {
const balance = await getCurrentBallanceWallet(transactionDetails?.origin_customer.id);
if (balance !== null) {
setCurrentBalance(balance);
}
}}
>
Generate Current Balance
</Button>
</div> </div>
)} )}
@ -493,16 +590,43 @@ const DetailTransaction = () => {
{!transactionDetails?.transfer ? ( {!transactionDetails?.transfer ? (
<div className="text-sm text-gray-500">No Data available</div> <div className="text-sm text-gray-500">No Data available</div>
) : ( ) : (
<div className="grid grid-cols-2 gap-4"> <>
<div> <div className="grid grid-cols-2 gap-4">
<p className="text-sm text-gray-500">Name</p> <div>
<p className="font-medium">{transactionDetails.transfer.destination_wallet.name}</p> <p className="text-sm text-gray-500">Name</p>
<p className="font-medium">{transactionDetails.transfer.destination_wallet.name}</p>
</div>
<div>
<p className="text-sm text-gray-500">Description</p>
<p className="font-medium">{transactionDetails.transfer.destination_wallet.description}</p>
</div>
<div>
<p className="text-sm text-gray-500">Current Balance</p>
<p className="font-medium">
{currentBalanceTransfer !== null
? new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(currentBalanceTransfer)
: '-'}
</p>
</div>
</div> </div>
<div> <Button
<p className="text-sm text-gray-500">Description</p> className="btn btn-primary"
<p className="font-medium">{transactionDetails.transfer.destination_wallet.description}</p> type="button"
</div> onClick={async () => {
</div> const customerId = transactionDetails?.transfer?.destination_customer?.id;
const balance = await getCurrentBallanceWalletTransfer(customerId);
if (balance !== null) {
setCurrentBalanceTransfer(balance); // ✅ INI YANG BENAR
}
}}
>
Generate Current Balance
</Button>
</>
)} )}
</div> </div>
)} )}

View File

@ -61,9 +61,9 @@ const ListToolbar = () => {
useEffect(() => { useEffect(() => {
const today = new Date(); const today = new Date();
const firstDayOfMonth = new Date(today.getFullYear(), today.getMonth(), 1); // const firstDayOfMonth = new Date(today.getFullYear(), today.getMonth(), 1);
settrxDate({ settrxDate({
from: formatDate(firstDayOfMonth), from: formatDate(today),
to: formatDate(today), to: formatDate(today),
}); });
}, []); }, []);
@ -152,7 +152,7 @@ const ListToolbar = () => {
<KeenIcon icon="magnifier" /> <KeenIcon icon="magnifier" />
<input <input
type="text" type="text"
placeholder="Search Transaction ID" placeholder="Search Transaction ID, MSISDN, Or Full Name Customer"
value={searchValue} value={searchValue}
onChange={(event) => setSearchValue(event.target.value)} onChange={(event) => setSearchValue(event.target.value)}
/> />
@ -167,14 +167,14 @@ const ListToolbar = () => {
onClick={() => { onClick={() => {
// Preserve trxDate values (from, to) and reset others // Preserve trxDate values (from, to) and reset others
const today = new Date(); const today = new Date();
const firstDayOfMonth = new Date(today.getFullYear(), today.getMonth(), 1); // const firstDayOfMonth = new Date(today.getFullYear(), today.getMonth(), 1);
// Only reset filters excluding from and to // Only reset filters excluding from and to
setSearchValue(''); setSearchValue('');
// setStatusValue(''); // setStatusValue('');
setTypeValue(''); setTypeValue('');
settrxDate({ settrxDate({
from: formatDate(firstDayOfMonth), from: formatDate(today),
to: formatDate(today), to: formatDate(today),
}); });

View File

@ -265,7 +265,6 @@ const TransactionProvider = ({ children }: { children: React.ReactNode }) => {
}); });
} }
// Build filter object dynamically
const formattedFilter: any = { const formattedFilter: any = {
"Transactions.transaction_date": { "Transactions.transaction_date": {
from: `${startdate} 00:00:00`, from: `${startdate} 00:00:00`,
@ -274,9 +273,11 @@ const TransactionProvider = ({ children }: { children: React.ReactNode }) => {
}; };
if (transactioncode) { if (transactioncode) {
formattedFilter["Transactions.code"] = { formattedFilter["$or"] = [
like: `%${transactioncode}%` { "Transactions.code": { like: `%${transactioncode}%` } },
}; { "Transactions.msisdn": { like: `${transactioncode}` } },
{ "origin_customer.fullname": { like: `${transactioncode}` } }
];
} }
// if (transactionstatus) { // if (transactionstatus) {

View File

@ -11,7 +11,7 @@
<div className="flex flex-wrap gap-2 lg:gap-5 w-full"> <div className="flex flex-wrap gap-2 lg:gap-5 w-full">
<div className="flex justify-between w-full items-center"> <div className="flex justify-between w-full items-center">
<div className="flex w-[50%] gap-3 items-center"> <div className="flex w-[50%] gap-3 items-center">
<label className="input input-sm w-1/3"> {/* <label className="input input-sm w-1/3">
<KeenIcon icon="magnifier" /> <KeenIcon icon="magnifier" />
<input <input
type="text" type="text"
@ -19,7 +19,7 @@
value={(table.getColumn('name')?.getFilterValue() as string) ?? ''} value={(table.getColumn('name')?.getFilterValue() as string) ?? ''}
onChange={(event) => table.getColumn('name')?.setFilterValue(event.target.value)} onChange={(event) => table.getColumn('name')?.setFilterValue(event.target.value)}
/> />
</label> </label> */}
</div> </div>
<div className="flex gap-3 items-center"> <div className="flex gap-3 items-center">
<Button <Button

View File

@ -23,6 +23,29 @@ interface TransferType {
const API_URL = apiConfig.service_transaction; const API_URL = apiConfig.service_transaction;
const typeLabelMap: Record<string, string> = {
D: 'Disbursement',
O: 'Other',
CA: 'Change Group Emoney Customer to Agent',
AC: 'Change Group Emoney Agent to Customer',
PC: 'Change Group Point Agent to Customer',
PA: 'Change Group Point Customer to Agent',
CE: 'Return Customer Emoney',
AD: 'Return Agent Deposit',
AM: 'Return Agent Merchant',
AE: 'Return Agent Emoney',
R: 'Reward Point',
TE: 'Top Up Escrow',
TM: 'Top Up Master Agent',
TA: 'Top Up Agent',
PL: 'Purchase Loja',
DE: 'Disbursment Escrow',
DM: 'Disbursment Master Agent',
DA: 'Disbursment Agent',
WI: 'Withdraw Merchant',
IC: 'Income Merchant'
};
const ListToolbar = () => { const ListToolbar = () => {
const { table, reload } = useDataGrid(); const { table, reload } = useDataGrid();
const { handleAddDialog } = useManageTransferTypeContext(); const { handleAddDialog } = useManageTransferTypeContext();
@ -31,39 +54,16 @@ const ListToolbar = () => {
const [searchValue, setSearchValue] = useState(''); const [searchValue, setSearchValue] = useState('');
const [statusTypes, setStatusTypes] = useState(''); const [statusTypes, setStatusTypes] = useState('');
const [walletOrigins, setWalletOrigin] = useState('');
const [walletDestinations, setWalletDestination] = useState('');
const [approval, setApproval] = useState(''); const [approval, setApproval] = useState('');
const unique = (arr: string[]) => Array.from(new Set(arr)); const unique = (arr: string[]) => Array.from(new Set(arr));
const types = unique(transferTypes.map((t) => t.type)); const types = unique(transferTypes.map((t) => t.type));
const origins = unique(transferTypes.map((t) => t.wallet_origin.name));
const destinations = unique(transferTypes.map((t) => t.wallet_destination.name)); const handleKeyDown = (event: React.KeyboardEvent) => {
const typeLabelMap: Record<string, string> = { if (event.key === 'Enter') {
D: 'Disbursement', table.getColumn('name')?.setFilterValue(searchValue);
O: 'Other', table.setPageIndex(0);
CA: 'Change Group Emoney Customer to Agent', }
AC: 'Change Group Emoney Agent to Customer',
PC: 'Change Group Point Agent to Customer',
PA: 'Change Group Point Customer to Agent',
CE: 'Return Customer Emoney',
AD: 'Return Agent Deposit',
AM: 'Return Agent Merchant',
AE: 'Return Agent Emoney',
R: 'Reward Point',
TE: 'Top Up Escrow',
TM: 'Top Up Master Agent',
TA: 'Top Up Agent',
PL: 'Purchase Loja',
DE: 'Disbursment Escrow',
DM: 'Disbursment Master Agent',
DA: 'Disbursment Agent',
WI: 'Withdraw Merchant',
IC: 'Income Merchant'
};
const approvalLabelMap: Record<string, string> = {
Y: 'Yes',
N: 'No'
}; };
useEffect(() => { useEffect(() => {
@ -72,27 +72,15 @@ const ListToolbar = () => {
table.setPageIndex(0); table.setPageIndex(0);
}, 200); }, 200);
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, [searchValue]); }, [searchValue, table]);
useEffect(() => { useEffect(() => {
table.getColumn('type')?.setFilterValue(statusTypes); table.getColumn('type')?.setFilterValue(statusTypes);
}, [statusTypes]); }, [statusTypes, table]);
useEffect(() => {
table
.getColumn('wallet_origin')
?.setFilterValue(walletOrigins === '__all__' ? '' : walletOrigins);
}, [walletOrigins]);
useEffect(() => {
table
.getColumn('wallet_destination')
?.setFilterValue(walletDestinations === '__all__' ? '' : walletDestinations);
}, [walletDestinations]);
useEffect(() => { useEffect(() => {
table.getColumn('status_approval')?.setFilterValue(approval); table.getColumn('status_approval')?.setFilterValue(approval);
}, [approval]); }, [approval, table]);
const fetchTransferTypes = async () => { const fetchTransferTypes = async () => {
try { try {
@ -115,62 +103,89 @@ const ListToolbar = () => {
const handleClearFilter = () => { const handleClearFilter = () => {
setSearchValue(''); setSearchValue('');
setStatusTypes('__all__'); setStatusTypes('');
setWalletOrigin('__all__'); setApproval('');
setWalletDestination('__all__');
setApproval('__all__');
table.getColumn('name')?.setFilterValue(''); table.getColumn('name')?.setFilterValue('');
table.getColumn('type')?.setFilterValue(''); table.getColumn('type')?.setFilterValue('');
table.getColumn('wallet_origin')?.setFilterValue('');
table.getColumn('wallet_destination')?.setFilterValue('');
table.getColumn('status_approval')?.setFilterValue(''); table.getColumn('status_approval')?.setFilterValue('');
table.setPageIndex(0);
setTimeout(() => {
table.setPageIndex(0);
reload();
}, 0);
}; };
const renderSelect = (
placeholder: string,
value: string,
onChange: (val: string) => void,
options: string[],
labelMap?: Record<string, string>
) => (
<div className="min-w-[220px]">
<Select value={value === '' ? undefined : value} onValueChange={onChange}>
<SelectTrigger className="h-8 text-sm">
<SelectValue placeholder={placeholder} />
</SelectTrigger>
<SelectContent>
{options.map((opt) => (
<SelectItem key={opt} value={opt}>
{labelMap?.[opt] ?? opt}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
return ( return (
<div className="card-header flex-wrap gap-2 border-b-0 px-5"> <div className="card-header flex-wrap border-b-0 px-5">
<div className="flex flex-wrap gap-2 lg:gap-5 w-full"> <div className="flex flex-wrap gap-1 w-full">
<div className="flex justify-between w-full items-center mb-3"> <div className="flex justify-between w-full items-center mb-3">
<div className="flex w-[34%] gap-2 items-center"> <div className="flex flex-nowrap items-end gap-3 w-full overflow-x-auto">
<label className="input input-sm w-full text-sm"> {/* Fixed Search Input dengan icon yang selalu terlihat */}
<KeenIcon icon="magnifier" /> <div className="relative w-48 shrink-0">
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 pointer-events-none ">
<KeenIcon icon="magnifier" />
</div>
<input <input
type="text" type="text"
placeholder="Search Transaction Type Name" placeholder="Search Name"
value={searchValue} value={searchValue}
onChange={(e) => setSearchValue(e.target.value)} onChange={(e) => setSearchValue(e.target.value)}
className="input input-sm w-full pl-9 text-ellipsis overflow-hidden"
onKeyDown={handleKeyDown}
/> />
</label> </div>
<div className="min-w-[220px] shrink-0">
<Select
key={`type-filter-${statusTypes}`}
value={statusTypes}
onValueChange={setStatusTypes}
>
<SelectTrigger className="h-8">
<SelectValue placeholder="Select Status Transaction Type" className="text-ellipsis overflow-hidden" />
</SelectTrigger>
<SelectContent className="max-h-64 overflow-y-auto">
{types.map((opt) => (
<SelectItem key={opt} value={opt} className="truncate">
{typeLabelMap[opt] ?? opt}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="min-w-[220px] shrink-0">
<Select
key={`approval-filter-${approval}`}
value={approval}
onValueChange={setApproval}
>
<SelectTrigger className="h-8">
<SelectValue placeholder="Select Status Approval" className="text-ellipsis overflow-hidden" />
</SelectTrigger>
<SelectContent className="max-h-64 overflow-y-auto">
<SelectItem value="Y" className="truncate">
Yes
</SelectItem>
<SelectItem value="N" className="truncate">
No
</SelectItem>
</SelectContent>
</Select>
</div>
<DefaultTooltip title={'Reset Filter'} placement={'top'}>
<Button
variant="outline"
className="h-7.5 disabled:bg-gray-400 shrink-0"
onClick={handleClearFilter}
>
<KeenIcon icon="arrow-circle-left" />
</Button>
</DefaultTooltip>
</div> </div>
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center ml-2 shrink-0">
<Button variant="outline" className="h-8 px-3 text-xs" onClick={handleClearFilter}>
Clear
</Button>
<Button <Button
variant="outline" variant="outline"
className="h-8 px-3 text-xs" className="h-8 px-3 text-xs"
@ -186,33 +201,6 @@ const ListToolbar = () => {
</div> </div>
</div> </div>
{/* Filter bar */}
<div className="flex flex-wrap gap-2 w-full">
{renderSelect(
'Select Status Transaction Type',
statusTypes,
setStatusTypes,
types,
typeLabelMap
)}
{renderSelect(
'Select Status Approval',
approval,
setApproval,
['Y', 'N'],
approvalLabelMap
)}
{/* {renderSelect('Select Origin Wallet', walletOrigins, setWalletOrigin, origins)}
{renderSelect(
'Select Destination Wallet',
walletDestinations,
setWalletDestination,
destinations
)} */}
{/* {renderSelect('Origin', walletOrigins, setWalletOrigin, origins)}
{renderSelect('Destination', walletDestinations, setWalletDestination, destinations)} */}
</div>
</div> </div>
</div> </div>
); );

View File

@ -212,7 +212,7 @@ const ManageTransferTypeContextProvider = ({ children }: { children: React.React
header: ({ column }) => ( header: ({ column }) => (
<DataGridColumnHeader title="Status Transaction Type" column={column} /> <DataGridColumnHeader title="Status Transaction Type" column={column} />
), ),
enableSorting: true, enableSorting: false,
enableHiding: false, enableHiding: false,
meta: { headerClassName: 'w-[250px]' } meta: { headerClassName: 'w-[250px]' }
}, },
@ -241,7 +241,7 @@ const ManageTransferTypeContextProvider = ({ children }: { children: React.React
header: ({ column }) => ( header: ({ column }) => (
<DataGridColumnHeader title="Status Kind" column={column} /> <DataGridColumnHeader title="Status Kind" column={column} />
), ),
enableSorting: true, enableSorting: false,
enableHiding: false, enableHiding: false,
meta: { headerClassName: 'w-[250px]' } meta: { headerClassName: 'w-[250px]' }
}, },

View File

@ -19,6 +19,13 @@ interface WalletProps {
name: string; name: string;
} }
const getOneMonthsAgo = () => {
const today = new Date();
return new Date(today.getFullYear(), today.getMonth() - 1, today.getDate());
};
const formatDate = (date: Date): string => date.toISOString().split('T')[0];
const ListToolbar = () => { const ListToolbar = () => {
const { table, reload } = useDataGrid(); const { table, reload } = useDataGrid();
const { GetData } = useCallApi(); const { GetData } = useCallApi();
@ -32,12 +39,10 @@ const ListToolbar = () => {
); );
const [wallets, setWallets] = useState<WalletProps[]>([]); const [wallets, setWallets] = useState<WalletProps[]>([]);
const formatDate = (date: Date): string => date.toISOString().split('T')[0];
useEffect(() => { useEffect(() => {
const today = new Date(); const today = new Date();
const firstDayOfMonth = new Date(today.getFullYear(), today.getMonth(), 1); const threeMonthsAgo = getOneMonthsAgo();
setDateRange({ from: formatDate(firstDayOfMonth), to: formatDate(today) }); setDateRange({ from: formatDate(threeMonthsAgo), to: formatDate(today) });
}, []); }, []);
useEffect(() => { useEffect(() => {
@ -45,7 +50,6 @@ const ListToolbar = () => {
table.getColumn('msisdn')?.setFilterValue(searchValue); table.getColumn('msisdn')?.setFilterValue(searchValue);
table.setPageIndex(0); table.setPageIndex(0);
}, 200); }, 200);
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, [searchValue, table]); }, [searchValue, table]);
@ -89,35 +93,50 @@ const ListToolbar = () => {
}, []); }, []);
const handleClearAllFilters = () => { const handleClearAllFilters = () => {
const today = new Date();
const oneMonthAgo = getOneMonthsAgo();
const resetDateRange = {
from: formatDate(oneMonthAgo),
to: formatDate(today)
};
setSearchValue(''); setSearchValue('');
setWalletId(''); setWalletId('');
setDateRange(resetDateRange);
table.getColumn('msisdn')?.setFilterValue('');
table.getColumn('id_wallet')?.setFilterValue('');
table.getColumn('CreatedAt')?.setFilterValue(resetDateRange);
setTimeout(() => {
table.setPageIndex(0);
reload();
}, 0);
};
const handleRefresh = () => {
const today = new Date(); const today = new Date();
const firstDayOfMonth = new Date(today.getFullYear(), today.getMonth(), 1); const threeMonthsAgo = getOneMonthsAgo();
setDateRange({ const resetDateRange = {
from: formatDate(firstDayOfMonth), from: formatDate(threeMonthsAgo),
to: formatDate(today) to: formatDate(today)
}); };
table.getAllColumns().forEach((column) => { setSearchValue('');
if (column.id !== 'CreatedAt') { setWalletId('');
column.setFilterValue(undefined); setDateRange(resetDateRange);
}
});
table.setColumnFilters([{ id: 'CreatedAt', value: resetDateRange }]);
table.setPageIndex(0); table.setPageIndex(0);
reload();
table.getColumn('CreatedAt')?.setFilterValue({
from: formatDate(firstDayOfMonth),
to: formatDate(today)
});
}; };
return ( return (
<div className="card-header flex-wrap gap-2 border-b-0 px-5"> <div className="card-header flex-wrap gap-2 border-b-0 px-5">
<div className="flex flex-wrap gap-2 lg:gap-5 w-full"> <div className="flex flex-wrap gap-2 lg:gap-5 w-full">
<div className="flex justify-between w-full items-center"> <div className="flex justify-between w-full items-center">
<div className="flex gap-3 items-center flex-wrap"> <div className="flex flex-wrap items-end gap-3 w-full">
<label className="input input-sm w-[160px]"> <label className="input input-sm w-[160px]">
From From
<input <input
@ -136,24 +155,20 @@ const ListToolbar = () => {
/> />
</label> </label>
<label className="input input-sm w-1/3"> <label className="input input-sm w-[160px]">
<KeenIcon icon="magnifier" /> <KeenIcon icon="magnifier" />
<input <input
type="text" type="text"
placeholder="Search MSISDN" placeholder="Search MSISDN"
value={searchValue} value={searchValue}
onChange={(e) => setSearchValue(e.target.value)} onChange={(e) => setSearchValue(e.target.value)}
className="overflow-hidden text-ellipsis w-full"
/> />
</label> </label>
<div className="w-[220px]"> <div className="w-[160px]">
<Select <Select value={walletId} onValueChange={(value) => setWalletId(value)}>
value={walletId} <SelectTrigger className="h-[32px]">
onValueChange={(value) => {
setWalletId(value);
}}
>
<SelectTrigger>
<SelectValue placeholder="Select Wallet" /> <SelectValue placeholder="Select Wallet" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@ -165,25 +180,20 @@ const ListToolbar = () => {
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<DefaultTooltip title={'Reset Filter'} placement={'top'}>
<Button variant="outline" size="sm" onClick={handleClearAllFilters}> <Button
Clear Filter variant="outline"
</Button> className="h-8 disabled:bg-gray-400"
onClick={handleClearAllFilters}
>
<KeenIcon icon="arrow-circle-left" />
</Button>
</DefaultTooltip>
</div> </div>
<div className="flex gap-3 items-center"> <div className="flex gap-3 items-center">
<DefaultTooltip title={'Refresh'} placement={'top'}> <DefaultTooltip title={'Refresh'} placement={'top'}>
<Button <Button variant="outline" className="h-7.5" onClick={handleRefresh}>
variant="outline"
className="h-7.5"
onClick={() => {
setSearchValue('');
setWalletId('');
table.setColumnFilters((prev) => prev.filter((f) => f.id === 'CreatedAt'));
table.setPageIndex(0);
reload();
}}
>
<KeenIcon icon="arrows-circle" /> <KeenIcon icon="arrows-circle" />
</Button> </Button>
</DefaultTooltip> </DefaultTooltip>