diff --git a/src/pages/transaction/approval-transaction/ApprovalTransaction.tsx b/src/pages/transaction/approval-transaction/ApprovalTransaction.tsx new file mode 100644 index 0000000..e6aa68b --- /dev/null +++ b/src/pages/transaction/approval-transaction/ApprovalTransaction.tsx @@ -0,0 +1,31 @@ +import { Container, DataGridInner } from '@/components'; +import { ApprovalTransactionProvider } from './hooks/ApprovalTransactionContext'; +import { Breadcrumbs, Link } from '@mui/material'; + +const ApprovalTransaction = () => { + return ( + + +

TRANSACTION

+ + + Dashboard + + + + Transaction + + + + Approval Transaction + + +
+ +
+
+
+ ); +}; + +export default ApprovalTransaction; diff --git a/src/pages/transaction/approval-transaction/blocks/DetailApprovalTransaction.tsx b/src/pages/transaction/approval-transaction/blocks/DetailApprovalTransaction.tsx new file mode 100644 index 0000000..e0d0ecc --- /dev/null +++ b/src/pages/transaction/approval-transaction/blocks/DetailApprovalTransaction.tsx @@ -0,0 +1,538 @@ +import { useTransactionContext } from '../hooks/useApprovalTransactionContext'; +import { useCallApi } from '@/hooks'; +import { apiConfig } from '@/config/api.config'; +import { useEffect, useState } from 'react'; +import { + Dialog, + DialogBody, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle +} from '@/components/ui/dialog'; + +const API_URL = apiConfig.transaction; + +const DetailApprovalTransaction = () => { + const { GetData } = useCallApi(); + const { + showDetailDialog, + setShowDetailDialog, + selectedTransactionId + } = useTransactionContext(); + + const [transactionDetails, setTransactionDetails] = useState(null); + + useEffect(() => { + const fetchTransactionDetails = async () => { + if (selectedTransactionId) { + try { + const response = await GetData(`${API_URL}/transaction/history/detail/${selectedTransactionId}`, { + id: selectedTransactionId + }); + // console.log(response?.data); + setTransactionDetails(response?.data); + } catch (error) { + console.error('Error fetching transaction', error); + } + } + }; + + if (showDetailDialog && selectedTransactionId) { + fetchTransactionDetails(); + } + }, [showDetailDialog, selectedTransactionId, GetData]); + + const [activeTab, setActiveTab] = useState('detail'); // 'detail', 'log', 'approve' + + return ( + + + + Transaction Details + + + {/* Tabs Navigation */} +
+ + + + + +
+ + {/* Tab Content */} +
+ {activeTab === 'detail' && transactionDetails?.kind === 'P' && ( +
+

+ Transaction Information + + Info + +

+
+
+

Transaction Date

+

{transactionDetails?.transaction_date}

+
+
+

Full Name

+

{transactionDetails?.origin_customer.fullname}

+
+
+

Amount

+

{new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(transactionDetails?.purchase.amount)}

+
+
+

Fee

+

+ {transactionDetails?.kind === 'P' + ? transactionDetails?.purchase.fee_amount.toLocaleString('en-US', { style: 'currency', currency: 'USD' }) + : transactionDetails?.transfer.fee_amount.toLocaleString('en-US', { style: 'currency', currency: 'USD' })} +

+
+
+

Status

+

+ {(() => { + let status; + if (transactionDetails?.status === 'C') { + status = 'COMPLETE'; + } else if (transactionDetails?.status === 'F') { + status = 'FAILED'; + } else if (transactionDetails?.status === 'O') { + status = 'ON PROCESS'; + } else { + status = 'PENDING'; + } + return status; + })()} +

+
+
+

Transaction Type

+

+ {(() => { + let kind; + if (transactionDetails?.kind === 'T') { + kind = 'TRANSFER'; + } else if (transactionDetails?.kind === 'P') { + kind = 'PURCHASE'; + } else if (transactionDetails?.kind === 'W') { + kind = 'WITHDRAW'; + } else if (transactionDetails?.kind === 'U') { + kind = 'TOP UP'; + } else if (transactionDetails?.kind === 'R') { + kind = 'RETURN'; + } + return kind; + })()} +

+
+
+

Description

+

{transactionDetails?.description}

+
+
+

Name

+

{transactionDetails?.type.name}

+
+
+ +

+ Origin Wallet + + Wallet + +

+
+
+

Name

+

{transactionDetails?.origin_wallet.name}

+
+
+

Description

+

{transactionDetails?.origin_wallet.description}

+
+
+ +

+ Purchase + + Purchase + +

+
+
+

Amount

+

{new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(transactionDetails?.purchase.amount)}

+
+
+

Cashback

+

{new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(transactionDetails?.purchase.cashback)}

+
+
+

Cashback Point

+

{transactionDetails?.purchase.cashback_point}

+
+
+

Fee Amount

+

{new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(transactionDetails?.purchase.fee_amount)}

+
+
+
+ )} + + {activeTab === 'detail' && transactionDetails?.kind != 'P' && transactionDetails?.transfer!=null && ( +
+

+ Transaction Information + + Info + +

+
+
+

Transaction Date

+

{transactionDetails?.transaction_date}

+
+
+

Full Name

+

{transactionDetails?.origin_customer.fullname}

+
+
+

Amount

+

{new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(transactionDetails?.transfer.amount)}

+
+
+

Fee

+

+ {transactionDetails?.transfer.fee_amount.toLocaleString('en-US', { style: 'currency', currency: 'USD' })} +

+
+
+

Status

+

+ {(() => { + let status; + if (transactionDetails?.status === 'C') { + status = 'COMPLETE'; + } else if (transactionDetails?.status === 'F') { + status = 'FAILED'; + } else if (transactionDetails?.status === 'O') { + status = 'ON PROCESS'; + } else { + status = 'PENDING'; + } + return status; + })()} +

+
+
+

Transaction Type

+

+ {(() => { + let kind; + if (transactionDetails?.kind === 'T') { + kind = 'TRANSFER'; + } else if (transactionDetails?.kind === 'P') { + kind = 'PURCHASE'; + } else if (transactionDetails?.kind === 'W') { + kind = 'WITHDRAW'; + } else if (transactionDetails?.kind === 'U') { + kind = 'TOP UP'; + } else if (transactionDetails?.kind === 'R') { + kind = 'RETURN'; + } + return kind; + })()} +

+
+
+

Description

+

{transactionDetails?.description}

+
+
+

Name

+

{transactionDetails?.type.name}

+
+
+

Reference

+

{transactionDetails?.transfer.reference}

+
+
+

Destination Iban

+

{transactionDetails?.transfer.destination_iban}

+
+
+ +

+ Destination Wallet + + Wallet + +

+
+
+

Name

+

{transactionDetails?.transfer.destination_wallet.name}

+
+
+

Description

+

{transactionDetails?.transfer.destination_wallet.description}

+
+
+ +

+ Destination Customer + + Destination Customer + +

+
+
+

Name

+

{transactionDetails?.transfer.destination_customer.fullname}

+
+
+

MSISDN

+

{transactionDetails?.transfer.destination_customer.msisdn}

+
+
+

Email

+

{transactionDetails?.transfer.destination_customer.email}

+
+
+

MSISDN

+

{transactionDetails?.transfer.destination_customer.username}

+
+
+ +

+ Origin Wallet + + Wallet + +

+
+
+

Name

+

{transactionDetails?.origin_wallet.name}

+
+
+

Description

+

{transactionDetails?.origin_wallet.description}

+
+
+
+ )} + + + {activeTab === 'origincustomer' && ( +
+

Origin Customer

+
+
+

Full Name

+

{transactionDetails?.origin_customer.fullname}

+
+
+

Phone Number

+

{transactionDetails?.origin_customer.msisdn}

+
+
+

Email

+

{transactionDetails?.origin_customer.email}

+
+
+

Username

+

{transactionDetails?.origin_customer.username}

+
+
+
+ )} + + {activeTab === 'log' && ( +
+

Transaction Logs

+
+ + + + + + + + + + + + + {transactionDetails?.log && transactionDetails?.log.length > 0 ? ( + transactionDetails.log.map((log: { request_endpoint: string, status: string; request_date: string; response_date: string; request_body: string; response_body: string; response_code: number }, index: number) => ( + + + + + + + + + )) + ) : ( + + + + )} + +
StatusRequest DateRequest BodyResponse BodyResponse CodeRequest End Point
+ {(() => { + let status; + if (log.status === 'P') { + status = 'PENDING'; + } else if (log.status === 'O') { + status = 'ON PROCESS'; + } else if (log.status === 'F') { + status = 'FAILED'; + } else if (log.status === 'C') { + status = 'COMPLETE'; + } + return status; + })()} + {log.request_date ?? '-'}{log.response_date ?? '-'}{log.request_body ?? '-'}{log.response_body ?? '-'}{log.request_endpoint ?? '-'}
+ No logs available +
+
+
+ )} + + {activeTab === 'approve' && ( +
+

Approval Logs

+ {transactionDetails?.log_approve.length === 0 ? ( +

No data available

+ ) : ( +
+ + + + + + + + + + {transactionDetails?.log_approve && transactionDetails?.log_approve.length > 0 ? ( + transactionDetails.log_approve.map((log: { created_at: string; status: string; updated_at: string }, index: number) => ( + + + + + + )) + ) : ( + + + + )} + +
StatusCreated AtUpdated At
+ {(() => { + let status; + if (log.status === 'W') { + status = 'WAITING'; + } else if (log.status === 'Y') { + status = 'APPROVE'; + } else if (log.status === 'N') { + status = 'REJECT'; + } else if (log.status === 'T') { + status = 'NO NEED'; + } + return status; + })()} + {log.created_at}{log.updated_at}
+ No logs available +
+
+ )} +
+ )} + + {activeTab === 'p24' && ( +
+

P24 Logs

+ {transactionDetails?.p24.length === 0 ? ( +

No data available

+ ) : ( +
+ + + + + + + + + + + + + {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) => ( + + + + + + + + + )) + ) : ( + + + + )} + +
TypeRequest DateRequest BodyResponse BodyResponse CodeRequest Endpoint
{log.type}{log.request_date}{log.response_date}{log.request_body}{log.response_body}{log.request_endpoint ?? '-'}
+ No logs available +
+
+ )} +
+ )} + +
+
+
+
+ ); +}; + +export default DetailApprovalTransaction; diff --git a/src/pages/transaction/approval-transaction/blocks/ListToolbar.tsx b/src/pages/transaction/approval-transaction/blocks/ListToolbar.tsx new file mode 100644 index 0000000..3247955 --- /dev/null +++ b/src/pages/transaction/approval-transaction/blocks/ListToolbar.tsx @@ -0,0 +1,82 @@ +import { DefaultTooltip, KeenIcon, useDataGrid } from '@/components'; +import { useTransactionContext } from '../hooks/useApprovalTransactionContext'; +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/approval-transaction/hooks/ApprovalTransactionContext.tsx b/src/pages/transaction/approval-transaction/hooks/ApprovalTransactionContext.tsx new file mode 100644 index 0000000..91c87c2 --- /dev/null +++ b/src/pages/transaction/approval-transaction/hooks/ApprovalTransactionContext.tsx @@ -0,0 +1,268 @@ +import { DataGridColumnHeader, DataGridProvider, KeenIcon } from '@/components'; +import { Toaster } from '@/components/ui/sonner'; +import { apiConfig } from '@/config/api.config'; +import { ColumnDef } from '@tanstack/react-table'; +import axios from 'axios'; +import React, { createContext, useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallApi } from '@/hooks'; +import ListToolbar from '../blocks/ListToolbar'; +import { Button } from '@/components/ui/button'; +import { useNavigate } from 'react-router'; +import DetailApprovalTransaction from '../blocks/DetailApprovalTransaction'; + +interface ApprovalTransactionProps { + id: number; + name: string; +} + +interface ContextProps { + getTransactionLists: ( + limit: number, + page: number, + with_deleted: boolean, + order_field: any, + order_direction: any, + filter: any + ) => Promise<{ data: ApprovalTransactionProps[]; totalCount: number } | undefined>; + showDetailDialog: boolean; + setShowDetailDialog: React.Dispatch>; + selectedTransactionId: number | null; + setSelectedTransactionId: React.Dispatch>; +} + +const initialProps: ContextProps = { + getTransactionLists: async () => ({ data: [], totalCount: 0 }), + showDetailDialog: false, + setShowDetailDialog: () => { }, + selectedTransactionId: null, + setSelectedTransactionId: () => { } +}; + +const ManageApprovalTransactionContext = createContext(initialProps); +const API_URL = apiConfig.transaction; + +const ApprovalTransactionProvider = ({ children }: { children: React.ReactNode }) => { + const [showDetailDialog, setShowDetailDialog] = useState(false); + const [selectedTransactionId, setSelectedTransactionId] = useState(null); + const [transaction, setTransaction] = useState([]); + const { GetData } = useCallApi(); + const navigate = useNavigate(); + const handleNavigate = (path: string) => { + const url = navigate(`${API_URL}/transaction/history/${path}`); + }; + + const columns = useMemo[]>( + () => [ + // { + // accessorKey: 'transaction_date', + // header: ({ column }) => , + // enableSorting: false, + // enableHiding: false, + // meta: { + // headerClassName: 'w-[250px]' + // } + // }, + { + accessorKey: 'transaction_date', + header: ({ column }) => , + enableSorting: false, + enableHiding: false, + meta: { + headerClassName: 'w-[250px]' + }, + cell: ({ row }) => { + // Memformat tanggal dan waktu dari ISO ke format biasa (DD-MM-YYYY HH:MM:SS) + const transactionDate = new Date(row.original.transaction_date); + const formattedDateTime = transactionDate.toLocaleString('en-GB', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false // Gunakan format 24 jam + }); + return formattedDateTime; // Format DD-MM-YYYY HH:MM:SS (menggunakan waktu yang sudah ada) + } + }, + { + accessorKey: 'origin_customer.fullname', + header: ({ column }) => , + enableSorting: false, + enableHiding: false, + meta: { + headerClassName: 'w-[250px]' + } + }, + { + accessorFn: (row) => { + const purchaseAmount = row?.purchase?.amount; + const transferAmount = row?.transfer?.amount; + + return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(purchaseAmount ?? transferAmount ?? 0); + }, + id: 'amount', + header: ({ column }) => , + enableSorting: false, + enableHiding: false, + meta: { + headerClassName: 'w-[250px]', + }, + }, + { + accessorFn: (row) => { + const purchaseAmount = row?.purchase?.fee_amount; + const transferAmount = row?.transfer?.fee_amount; + + return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(purchaseAmount ?? transferAmount ?? 0); + }, + id: 'feeamount', + header: ({ column }) => , + enableSorting: false, + enableHiding: false, + meta: { + headerClassName: 'w-[250px]', + }, + }, + { + accessorFn: (row) => { + let status; + if (row.status === 'C') { + status = 'COMPLETE'; + } else if (row.status === 'F') { + status = 'FAILED'; + } else if (row.status === 'O') { + status = 'ON PROCESS'; + } else { + status = 'PENDING'; + } + return status; + }, + accessorKey: 'status', + header: ({ column }) => , + enableSorting: false, + enableHiding: false, + meta: { + headerClassName: 'w-[250px]', + }, + }, + { + accessorKey: 'description', + header: ({ column }) => , + enableSorting: false, + enableHiding: false, + meta: { + headerClassName: 'w-[250px]' + } + }, + { + accessorKey: 'type.name', + header: ({ column }) => , + enableSorting: false, + enableHiding: false, + meta: { + headerClassName: 'w-[250px]' + } + }, + { + id: 'actions', + header: ({ column }) => , + enableSorting: false, + enableHiding: false, + cell: (data) => { + const row = data.row.original; + return ( +
+ +
+ ); + }, + meta: { + headerClassName: 'w-[100px]', + cellClassName: 'text-center' + } + } + ], + []); + + const getTransactionLists = async (page: number, limit: number, sorting: any, filter: any) => { + try { + let startdate; + let enddate; + let formattedFilter; + + if (filter == undefined || filter.length == 0) { + const today = new Date(); + const nextWeek = new Date(); + nextWeek.setDate(today.getDate() + 7); + + startdate = today.toISOString().split('T')[0]; + enddate = nextWeek.toISOString().split('T')[0]; + } else if (filter != undefined || filter.length != 0) { + startdate = filter[0].value.from; + enddate = filter[0].value.to; + } + + formattedFilter = { + "Transactions.transaction_date": { + from: startdate + " 00:00:00", + to: enddate + " 23:59:59" + } + }; + + const response = await GetData(`${API_URL}/transaction/history`, { + limit, + page: page + 1, + with_deleted: false, + order_field: "Transactions.created_at", + order_direction: 'DESC', + filter: JSON.stringify(formattedFilter) + }); + + setTransaction(response?.data.list); + return { data: response?.data.list, totalCount: response?.data.total_count }; + } catch (error) { + console.error('Error fetching transaction', error); + } + }; + + return ( + + + + + } + layout={{ card: true }} + sorting={[{ id: 'id', desc: false }]} + serverSide={true} + onFetchData={({ pageIndex, pageSize, sorting, columnFilters }) => + getTransactionLists(pageIndex, pageSize, sorting, columnFilters) + } + > + {children} + + + ); +}; + +export { ApprovalTransactionProvider, ManageApprovalTransactionContext }; +export type { ApprovalTransactionProps }; diff --git a/src/pages/transaction/approval-transaction/hooks/useApprovalTransactionContext.tsx b/src/pages/transaction/approval-transaction/hooks/useApprovalTransactionContext.tsx new file mode 100644 index 0000000..3f93690 --- /dev/null +++ b/src/pages/transaction/approval-transaction/hooks/useApprovalTransactionContext.tsx @@ -0,0 +1,12 @@ +import { useContext } from 'react'; +import { ManageApprovalTransactionContext } from './ApprovalTransactionContext'; + +const useTransactionContext = () => { + const context = useContext(ManageApprovalTransactionContext); + + if (!context) throw new Error('useTransactionContext must be used within AuthProvider'); + + return context; +}; + +export { useTransactionContext }; diff --git a/src/pages/transaction/Transaction.tsx b/src/pages/transaction/history-transaction/Transaction.tsx similarity index 93% rename from src/pages/transaction/Transaction.tsx rename to src/pages/transaction/history-transaction/Transaction.tsx index 9456873..3afcc68 100644 --- a/src/pages/transaction/Transaction.tsx +++ b/src/pages/transaction/history-transaction/Transaction.tsx @@ -17,7 +17,7 @@ const Transaction = () => { - Transaction + History Transaction
diff --git a/src/pages/transaction/blocks/DetailTransaction.tsx b/src/pages/transaction/history-transaction/blocks/DetailTransaction.tsx similarity index 93% rename from src/pages/transaction/blocks/DetailTransaction.tsx rename to src/pages/transaction/history-transaction/blocks/DetailTransaction.tsx index ec5dd9f..5f3ad0d 100644 --- a/src/pages/transaction/blocks/DetailTransaction.tsx +++ b/src/pages/transaction/history-transaction/blocks/DetailTransaction.tsx @@ -208,8 +208,44 @@ const DetailTransaction = () => {
)} + {activeTab === 'detail' && transactionDetails?.kind === 'P' && ( +
+

+ Product Information + + Product Info + +

+
+
+

Product Name

+

{transactionDetails?.purchase.product.name}

+
+
+

Price Cash

+ {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(transactionDetails?.purchase.product.price_cash)} +
+
+

Price Point

+ {new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(transactionDetails?.purchase.product.price_point)} +
+
+

Product Type

+

{transactionDetails?.purchase.product.type}

+
+
+

Provider Name

+

{transactionDetails?.purchase.product.provider.description}

+
+
+

Provider Type

+

{transactionDetails?.purchase.product.provider.type}

+
+
+
+ )} - {activeTab === 'detail' && transactionDetails?.kind != 'P' && transactionDetails?.transfer!=null && ( + {activeTab === 'detail' && transactionDetails?.kind != 'P' && transactionDetails?.transfer != null && (

Transaction Information diff --git a/src/pages/transaction/blocks/ListToolbar.tsx b/src/pages/transaction/history-transaction/blocks/ListToolbar.tsx similarity index 100% rename from src/pages/transaction/blocks/ListToolbar.tsx rename to src/pages/transaction/history-transaction/blocks/ListToolbar.tsx diff --git a/src/pages/transaction/hooks/TransactionContext.tsx b/src/pages/transaction/history-transaction/hooks/TransactionContext.tsx similarity index 100% rename from src/pages/transaction/hooks/TransactionContext.tsx rename to src/pages/transaction/history-transaction/hooks/TransactionContext.tsx diff --git a/src/pages/transaction/hooks/useTransactionContext.tsx b/src/pages/transaction/history-transaction/hooks/useTransactionContext.tsx similarity index 100% rename from src/pages/transaction/hooks/useTransactionContext.tsx rename to src/pages/transaction/history-transaction/hooks/useTransactionContext.tsx diff --git a/src/routing/AppRoutingSetup.tsx b/src/routing/AppRoutingSetup.tsx index ae1b8ed..01bd4f6 100644 --- a/src/routing/AppRoutingSetup.tsx +++ b/src/routing/AppRoutingSetup.tsx @@ -8,7 +8,8 @@ import { ErrorsRouting } from '@/errors'; import DashboardHomePage from '@/pages/dashboards/home/DashboardHomePage'; import ManageUserPage from '@/pages/settings/user/manage-user/ManageUserPage'; -import Transaction from '@/pages/transaction/Transaction'; +import Transaction from '@/pages/transaction/history-transaction/Transaction'; +import ApprovalTransaction from '@/pages/transaction/approval-transaction/ApprovalTransaction'; 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'; @@ -85,6 +86,7 @@ const AppRoutingSetup = (): ReactElement => { } /> } /> + } /> } /> } /> } />