From 225e1304a58d0e7e3054aa585d20500f7186adde Mon Sep 17 00:00:00 2001 From: wayanrivan Date: Thu, 8 May 2025 13:49:27 +0700 Subject: [PATCH] withdraw saldo --- .../withdrawl-saldo/TransactionWithdraw.tsx | 359 ++++++++++++++++++ .../withdrawl-saldo/blocks/ListToolbar.tsx | 61 +++ .../hooks/TransactionWithdrawContext.tsx | 80 ++++ .../withdrawl-saldo/hooks/index.tsx | 2 + .../hooks/useTransactionWithdrawContext.tsx | 12 + src/routing/AppRoutingSetup.tsx | 3 + 6 files changed, 517 insertions(+) create mode 100644 src/pages/transaction/withdrawl-saldo/TransactionWithdraw.tsx create mode 100644 src/pages/transaction/withdrawl-saldo/blocks/ListToolbar.tsx create mode 100644 src/pages/transaction/withdrawl-saldo/hooks/TransactionWithdrawContext.tsx create mode 100644 src/pages/transaction/withdrawl-saldo/hooks/index.tsx create mode 100644 src/pages/transaction/withdrawl-saldo/hooks/useTransactionWithdrawContext.tsx diff --git a/src/pages/transaction/withdrawl-saldo/TransactionWithdraw.tsx b/src/pages/transaction/withdrawl-saldo/TransactionWithdraw.tsx new file mode 100644 index 0000000..9b30b1b --- /dev/null +++ b/src/pages/transaction/withdrawl-saldo/TransactionWithdraw.tsx @@ -0,0 +1,359 @@ +import { Alert, Container, DataGridInner } from '@/components'; +import { TransactionWithdrawProvider } from './hooks/TransactionWithdrawContext'; +import { Breadcrumbs, Link } from '@mui/material'; +import { Helmet } from 'react-helmet'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import { useState, useEffect, useRef } from 'react'; +import { useCallApi } from '@/hooks'; +import { apiConfig } from '@/config/api.config'; +import { toast } from 'sonner'; +import { getAuth } from '@/auth'; +import { RefreshCw } from 'lucide-react'; + +const TransactionWithdraw = () => { + const initialForm: { + msisdn: string; + amount: string; + pin: string; + purpose: string; + } = { + msisdn: '', + amount: '', + pin: '', + purpose: '' + }; + + const [form, setForm] = useState(initialForm); + const [wallets, setWallets] = useState([]); + 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 fetchWallets = async () => { + try { + const response = await GetData( + `${API_URL_WALLET}/dashboard/balance/account/${parsedUser.customer.id}`, + {} + ); + if (response?.status === true) { + setWallets(response.data || []); + } else { + toast.warning(response?.message || 'Failed to fetch wallet data'); + } + } catch (error) { + toast.warning('Failed to fetch wallet data'); + } + }; + + 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); + + 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: "20c8a690-dc02-463d-b391-324184d1fefa", + id_origin_customer: getAuth()?.id + }); + if (response?.status == true) { + await fetchWallets(); + 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); + // TODO: Kirim ke backend atau proses lainnya + }; + + const ResetForm = () => { + setForm(initialForm); + setAlert({ show: false, message: '' }); + setSearchTerm(''); + }; + + 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(() => { + fetchWallets(); + 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 ( + <> + + TPAY | Transaction Withdraw Saldo + + + +

+ MANAGE TRANSACTION WITHDRAW SALDO +

+ + + Dashboard + + + Transaction + + + Withdraw Saldo + + + {/* Wallet Section */} +
+

Your Wallets

+
+ {wallets.map((wallet: any) => ( +
+

{wallet.wallet}

+

+ {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format( + wallet.amount + )} +

+
+ ))} +
+
+ + +
+
+ {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 TransactionWithdraw; diff --git a/src/pages/transaction/withdrawl-saldo/blocks/ListToolbar.tsx b/src/pages/transaction/withdrawl-saldo/blocks/ListToolbar.tsx new file mode 100644 index 0000000..013a15c --- /dev/null +++ b/src/pages/transaction/withdrawl-saldo/blocks/ListToolbar.tsx @@ -0,0 +1,61 @@ +import { DefaultTooltip, KeenIcon, useDataGrid } from '@/components'; +import { Button } from '@/components/ui/button'; +import { useCallback, useState, useEffect } from 'react'; +import { toast } from 'sonner'; + +const ListToolbar = () => { + const { table, reload } = useDataGrid(); + + // Set the initial state for trxDate + const [trxDate, settrxDate] = useState({ from: '', to: '' }); + + // Function to format date to YYYY-MM-DD + const formatDate = (date: Date): string => { + return date.toISOString().split('T')[0]; + }; + + // useEffect to set the default date values + useEffect(() => { + const today = new Date(); + const nextWeek = new Date(today); + nextWeek.setDate(today.getDate() + 7); + + settrxDate({ + from: formatDate(today), // Set 'from' to today + to: formatDate(nextWeek), // Set 'to' to 7 days later + }); + }, []); + + const handleFilterData = useCallback(() => { + try { + table.getColumn('transaction_date')?.setFilterValue(trxDate); + } catch (error) { + toast.error('Error applying filter'); + console.error('Error applying filter:', error); + } + }, [trxDate, table]); + + useEffect(() => { + if (trxDate.from && trxDate.to) { + handleFilterData(); + } + }, [trxDate]); + + return ( +
+
+
+
+ + + +
+
+
+
+ ); +}; + +export default ListToolbar; diff --git a/src/pages/transaction/withdrawl-saldo/hooks/TransactionWithdrawContext.tsx b/src/pages/transaction/withdrawl-saldo/hooks/TransactionWithdrawContext.tsx new file mode 100644 index 0000000..468c825 --- /dev/null +++ b/src/pages/transaction/withdrawl-saldo/hooks/TransactionWithdrawContext.tsx @@ -0,0 +1,80 @@ +import { DataGridColumnHeader, DataGridProvider, KeenIcon } from '@/components'; +import { Toaster } from '@/components/ui/sonner'; +import { toast } from 'sonner'; +import { apiConfig } from '@/config/api.config'; +import { ColumnDef } from '@tanstack/react-table'; +import { createContext, useCallback, useMemo, useState } from 'react'; +import ListToolbar from '../blocks/ListToolbar'; +import { useCallApi } from '@/hooks'; +import moment from 'moment'; + +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 TransactionWithdrawContext = 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 { reload } = useDataGrid(); + +const TransactionWithdrawProvider = ({ children }: { children: React.ReactNode }) => { + + return ( + + +
+ {children} +
+
+ ); +}; + +export { TransactionWithdrawProvider, TransactionWithdrawContext }; +export type { TransactionWithdrawProps }; diff --git a/src/pages/transaction/withdrawl-saldo/hooks/index.tsx b/src/pages/transaction/withdrawl-saldo/hooks/index.tsx new file mode 100644 index 0000000..3354df9 --- /dev/null +++ b/src/pages/transaction/withdrawl-saldo/hooks/index.tsx @@ -0,0 +1,2 @@ +export * from './TransactionWithdrawContext'; +export * from './useTransactionWithdrawContext'; diff --git a/src/pages/transaction/withdrawl-saldo/hooks/useTransactionWithdrawContext.tsx b/src/pages/transaction/withdrawl-saldo/hooks/useTransactionWithdrawContext.tsx new file mode 100644 index 0000000..64fe126 --- /dev/null +++ b/src/pages/transaction/withdrawl-saldo/hooks/useTransactionWithdrawContext.tsx @@ -0,0 +1,12 @@ +import { useContext } from 'react'; +import { TransactionWithdrawContext } from './TransactionWithdrawContext'; + +const useTransactionWithdrawContext = () => { + const context = useContext(TransactionWithdrawContext); + + if (!context) throw new Error('useTransactionWithdrawContext must be used within AuthProvider'); + + return context; +}; + +export { useTransactionWithdrawContext }; diff --git a/src/routing/AppRoutingSetup.tsx b/src/routing/AppRoutingSetup.tsx index fbb889e..d9cd336 100644 --- a/src/routing/AppRoutingSetup.tsx +++ b/src/routing/AppRoutingSetup.tsx @@ -12,6 +12,7 @@ import Transaction from '@/pages/transaction/history-transaction/Transaction'; import ApprovalTransaction from '@/pages/transaction/approval-transaction/ApprovalTransaction'; import TransactionTopup from '@/pages/transaction/topup/TransactionTopup'; import TransactionDisbursement from '@/pages/transaction/disbursement-saldo/TransactionDisbursement'; +import TransactionWithdraw from '@/pages/transaction/withdrawl-saldo/TransactionWithdraw'; import LogActivityPage from '@/pages/settings/user/log-activity/LogActivityPage'; import ManagePositionPage from '@/pages/settings/user/manage-position/ManagePositionPage'; import ManageAccount from '@/pages/account/manage-account/ManageAccount'; @@ -46,6 +47,7 @@ import FeedbackMemberMaster from '@/pages/members/feedback-member/FeedbackMember // DISBURSEMENT import HistoryTransactionDisbursement from '@/pages/disbursement/history-transaction/HistoryTransaction'; + // DISBURSEMENT const AppRoutingSetup = (): ReactElement => { @@ -102,6 +104,7 @@ const AppRoutingSetup = (): ReactElement => { } /> } /> } /> + } /> } /> } /> } />