From 5c6d834c199d1ebd446af14c6209cdae3adeb88d Mon Sep 17 00:00:00 2001 From: Wikzyy Date: Thu, 29 May 2025 16:49:10 +0700 Subject: [PATCH] [skip ci] add feature withdrawl emoney --- .../withdrawl-emoney/WithdrawalEmoney.tsx | 36 +++ .../withdrawl-emoney/blocks/WithdrawInput.tsx | 300 ++++++++++++++++++ .../TransactionWithdrawalEmoneyContext.tsx | 59 ++++ .../hooks/useTransactionWithdrawalEmoney.tsx | 12 + .../hooks/useTransactionWithdrawContext.tsx | 4 +- src/routing/AppRoutingSetup.tsx | 2 + 6 files changed, 411 insertions(+), 2 deletions(-) create mode 100644 src/pages/transaction/withdrawl-emoney/WithdrawalEmoney.tsx create mode 100644 src/pages/transaction/withdrawl-emoney/blocks/WithdrawInput.tsx create mode 100644 src/pages/transaction/withdrawl-emoney/hooks/TransactionWithdrawalEmoneyContext.tsx create mode 100644 src/pages/transaction/withdrawl-emoney/hooks/useTransactionWithdrawalEmoney.tsx diff --git a/src/pages/transaction/withdrawl-emoney/WithdrawalEmoney.tsx b/src/pages/transaction/withdrawl-emoney/WithdrawalEmoney.tsx new file mode 100644 index 0000000..d4476a4 --- /dev/null +++ b/src/pages/transaction/withdrawl-emoney/WithdrawalEmoney.tsx @@ -0,0 +1,36 @@ +import { Helmet } from 'react-helmet'; +import { TransactionWithdrawEmoneyProvider } from './hooks/TransactionWithDrawalEmoneyContext'; +import { Container } from '@/components'; +import { Breadcrumbs, Link } from '@mui/material'; +import WithdrawInput from './blocks/WithdrawInput'; + +const WithdrawalEmoney = () => { + return ( + <> + + TPAY | Withdrawal Emoney + + + +

+ TRANSACTION WITHDRAWAL EMONEY +

+ + + Dashboard + + + Transaction + + + Withdraw Saldo + + + +
+
+ + ); +}; + +export default WithdrawalEmoney; diff --git a/src/pages/transaction/withdrawl-emoney/blocks/WithdrawInput.tsx b/src/pages/transaction/withdrawl-emoney/blocks/WithdrawInput.tsx new file mode 100644 index 0000000..b68aef1 --- /dev/null +++ b/src/pages/transaction/withdrawl-emoney/blocks/WithdrawInput.tsx @@ -0,0 +1,300 @@ +import { getAuth } from '@/auth'; +import { Alert, Container } from '@/components'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { apiConfig } from '@/config/api.config'; +import { useCallApi } from '@/hooks'; +import { RefreshCw } from 'lucide-react'; +import { useEffect, useRef, useState } from 'react'; +import { toast } from 'sonner'; + +const WithdrawInput = () => { + const initialForm: { + msisdn: string; + amount: string; + pin: string; + purpose: string; + } = { + msisdn: '', + amount: '', + pin: '', + purpose: '' + }; + + const [form, setForm] = useState(initialForm); + const [isLoading, setIsLoading] = useState(false); + const [customerMsisdn, setCustomerMsisdn] = useState<{ value: string; label: string }[]>([]); + const [searchTerm, setSearchTerm] = useState(''); + const [dropdownOpen, setDropdownOpen] = useState(false); + const { GetData, PostData } = useCallApi(); + const [isSubmitting, setIsSubmitting] = useState(false); + const [showConfirmation, setShowConfirmation] = useState(false); + const parsedUser = getAuth()?.user; + const API_URL = apiConfig.transaction; + const API_URL_WALLET = apiConfig.service_wallet; + const API_URL_CUSTOMER = apiConfig.service_customer; + const dropdownRef = useRef(null); + + const [alert, setAlert] = useState({ + show: false, + message: '' + }); + + const ResetForm = () => { + setForm(initialForm); + setAlert({ show: false, message: '' }); + setSearchTerm(''); + }; + + const fetchCustomerMsisdn = async (sorting: any, filterValue: string) => { + const filter: any = + filterValue.trim().length === 0 + ? {} + : { + or: [ + { msisdn: { like: `%${filterValue}%` } }, + { fullname: { like: `%${filterValue}%` } } + ] + }; + + const query: any = { + limit: 100, + page: 1, + with_deleted: false, + order_field: sorting[0].id, + order_direction: sorting[0].desc ? 'DESC' : 'ASC' + }; + + if (filter && Object.keys(filter).length > 0) { + query.filter = JSON.stringify(filter); + // query.page = page + 1; + } + + try { + const response = await GetData(`${API_URL_CUSTOMER}/customer/list`, query); + setCustomerMsisdn( + response?.data.list.map((item: any) => ({ + value: item.msisdn, + label: `${item.msisdn} - ${item.fullname}` + })) + ); + } catch (error) { + toast.error('Failed to fetch customer msisdn'); + } finally { + setIsLoading(false); + } + }; + + const doPostData = async (form: typeof initialForm) => { + setIsSubmitting(true); + // console.log(getAuth()?.user.customer.id); + + try { + let response = await PostData(`${API_URL}/transaction/transfer`, { + msisdn_destination: form.msisdn, + amount: form.amount, + pin: form.pin, + purpose: form.purpose, + id_transaction_type: '380f270b-1f44-406f-a196-d6f784d68eb4', + id_origin_customer: getAuth()?.user.customer.id, + type: 'S' + }); + if (response?.status == true) { + toast.success('Success Request Topup'); + } else { + toast.error(`${response?.message?.message}`); + } + } catch (error: any) { + const errorMessage = + error?.response?.data?.message || error?.message || 'Something went wrong'; + toast.error(errorMessage); + setAlert({ show: true, message: errorMessage }); + } finally { + setIsSubmitting(false); + setShowConfirmation(false); + ResetForm(); + } + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (form.amount == '' || form.msisdn == '' || form.pin == '') { + setAlert({ + show: true, + message: 'Please fill in all required fields.' + }); + return; + } + setAlert({ show: false, message: '' }); + setShowConfirmation(true); + }; + + const handleCancelSubmit = () => { + setShowConfirmation(false); + }; + + const handleMsisdnSearch = (e: React.ChangeEvent) => { + setIsLoading(true); + setSearchTerm(e.target.value); + setDropdownOpen(true); + const timer = setTimeout(() => { + fetchCustomerMsisdn([{ id: 'msisdn', desc: false }], e.target.value); + }, 500); + return () => clearTimeout(timer); + }; + + const handleMsisdnSelect = (msisdn: string) => { + setForm({ ...form, msisdn }); + setDropdownOpen(false); + setSearchTerm(msisdn); + }; + + useEffect(() => { + fetchCustomerMsisdn([{ id: 'msisdn', desc: false }], ''); + + const handleClickOutside = (event: any) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { + setDropdownOpen(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + const filteredMsisdn = customerMsisdn + .filter((item) => item.label.toLowerCase().includes(searchTerm.toLowerCase())) + .slice(0, 10); + + return ( + <> + +
+
+ {alert.show && ( + +

{alert.message}

+
+ )} + {/* form */} +
+
+ + * +
+ setDropdownOpen(true)} + /> + {dropdownOpen && ( +
+ {filteredMsisdn.length > 0 ? ( + filteredMsisdn.map((item, index) => ( +
handleMsisdnSelect(item.value)} + > + {item.label} +
+ )) + ) : ( +
+ {isLoading ? 'Loading...' : 'No results found'} +
+ )} +
+ )} +
+
+ +
+ + * + { + const value = Number(e.target.value); + if (value >= 0) { + setForm({ ...form, amount: String(value) }); + } + }} + /> +
+
+ + * + setForm({ ...form, pin: e.target.value })} + /> +
+
+ + * + setForm({ ...form, purpose: e.target.value })} + /> +
+
+ +
+
+
+
+
+ + {showConfirmation && ( +
+
+

Confirm Transaction

+

+ Are you sure you want to withdraw saldo of{' '} + + {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format( + Number(form.amount) + )}{' '} + + ? +

+
+ + +
+
+
+ )} + + ); +}; + +export default WithdrawInput; diff --git a/src/pages/transaction/withdrawl-emoney/hooks/TransactionWithdrawalEmoneyContext.tsx b/src/pages/transaction/withdrawl-emoney/hooks/TransactionWithdrawalEmoneyContext.tsx new file mode 100644 index 0000000..9850401 --- /dev/null +++ b/src/pages/transaction/withdrawl-emoney/hooks/TransactionWithdrawalEmoneyContext.tsx @@ -0,0 +1,59 @@ +import { apiConfig } from '@/config/api.config'; +import { createContext } from 'react'; + +interface TransactionWithdrawProps { + id: string; + customers_id: string; + group_id: string; + username: string; + fullname: string; + email: string; + status: string; + created_at: Date; +} + +interface ContextProps {} + +const initialProps: ContextProps = {}; + +const TranscationWithdrawEmoneyContext = createContext(initialProps); +const API_URL = apiConfig.service_customer; + +type StatusCode = 'W' | 'Y' | 'N' | 'T'; + +interface StatusInfo { + label: string; + bg: string; + text: string; +} + +const statusMap: Record = { + W: { label: 'Waiting Approval', bg: 'bg-yellow-100', text: 'text-yellow-600' }, + T: { label: 'No Need', bg: 'bg-blue-100', text: 'text-blue-600' }, + N: { label: 'Reject', bg: 'bg-red-100', text: 'text-red-600' }, + Y: { label: 'Approve', bg: 'bg-green-100', text: 'text-green-600' } +}; + +export const renderStatusBadge = (statusRaw: string | null | undefined) => { + const status = statusRaw as StatusCode; + const { label, bg, text } = statusMap[status] ?? { + label: 'Unknown', + bg: 'bg-gray-100', + text: 'text-gray-600' + }; + + return ( + {label} + ); +}; + +const TransactionWithdrawEmoneyProvider = ({ children }: { children: React.ReactNode }) => { + return ( + +
{children}
+
+ ); +}; + +export { TransactionWithdrawEmoneyProvider, TranscationWithdrawEmoneyContext }; +export type { TransactionWithdrawProps }; diff --git a/src/pages/transaction/withdrawl-emoney/hooks/useTransactionWithdrawalEmoney.tsx b/src/pages/transaction/withdrawl-emoney/hooks/useTransactionWithdrawalEmoney.tsx new file mode 100644 index 0000000..93c361d --- /dev/null +++ b/src/pages/transaction/withdrawl-emoney/hooks/useTransactionWithdrawalEmoney.tsx @@ -0,0 +1,12 @@ +import { useContext } from 'react'; +import { TransactionWithdrawContext } from '../../withdrawl-saldo/hooks'; + +const useTransactionWithdrawalEmoney = () => { + const context = useContext(TransactionWithdrawContext); + + if (!context) throw new Error('useTransactionWithdrawalEmoney must be used within AuthProvider'); + + return context; +}; + +export { useTransactionWithdrawalEmoney }; diff --git a/src/pages/transaction/withdrawl-saldo/hooks/useTransactionWithdrawContext.tsx b/src/pages/transaction/withdrawl-saldo/hooks/useTransactionWithdrawContext.tsx index 64fe126..67e8288 100644 --- a/src/pages/transaction/withdrawl-saldo/hooks/useTransactionWithdrawContext.tsx +++ b/src/pages/transaction/withdrawl-saldo/hooks/useTransactionWithdrawContext.tsx @@ -1,8 +1,8 @@ import { useContext } from 'react'; -import { TransactionWithdrawContext } from './TransactionWithdrawContext'; +import { TranscationWithdrawEmoneyContext } from '../../withdrawl-emoney/hooks/TransactionWithDrawalEmoneyContext'; const useTransactionWithdrawContext = () => { - const context = useContext(TransactionWithdrawContext); + const context = useContext(TranscationWithdrawEmoneyContext); if (!context) throw new Error('useTransactionWithdrawContext must be used within AuthProvider'); diff --git a/src/routing/AppRoutingSetup.tsx b/src/routing/AppRoutingSetup.tsx index 1da4e34..ab42475 100644 --- a/src/routing/AppRoutingSetup.tsx +++ b/src/routing/AppRoutingSetup.tsx @@ -49,6 +49,7 @@ import AgentBalance from '@/pages/members/agent-balance/AgentBalance'; // DISBURSEMENT import HistoryTransactionDisbursement from '@/pages/disbursement/history-transaction/HistoryTransaction'; +import WithdrawalEmoney from '@/pages/transaction/withdrawl-emoney/WithdrawalEmoney'; // DISBURSEMENT @@ -110,6 +111,7 @@ const AppRoutingSetup = (): ReactElement => { } /> } /> } /> + } /> } /> } /> } />