diff --git a/src/components/confirm.tsx b/src/components/confirm.tsx index b3a5596..67e18e9 100644 --- a/src/components/confirm.tsx +++ b/src/components/confirm.tsx @@ -1,23 +1,21 @@ -import React from "react"; -import Dialog from "@mui/material/Dialog"; -import DialogActions from "@mui/material/DialogActions"; -import DialogContent from "@mui/material/DialogContent"; -import DialogTitle from "@mui/material/DialogTitle"; import Button from "@mui/material/Button"; +import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogDescription, DialogBody } from '@/components/ui/dialog'; -const ConfirmDialog = ({ open, onClose, title, content, onYes, onNo }: any) => { +const ConfirmDialog = ({ open, onClose, title, content, onYes, onNo, onOpenChange }: any) => { return ( - - {title && {title}} - {content && {content}} - - - - + + + + {title && {title}} + + {content && {content}} + + + + + + + ); }; diff --git a/src/components/loaders/LoaderTransparant.tsx b/src/components/loaders/LoaderTransparant.tsx new file mode 100644 index 0000000..b1eb099 --- /dev/null +++ b/src/components/loaders/LoaderTransparant.tsx @@ -0,0 +1,16 @@ +import { toAbsoluteUrl } from '@/utils'; + +const LoaderTransparant = () => { + return ( +
+ logo +
Loading...
+
+ ); +}; + +export { LoaderTransparant }; diff --git a/src/components/loaders/index.ts b/src/components/loaders/index.ts index 1d1dad1..1b8a61e 100644 --- a/src/components/loaders/index.ts +++ b/src/components/loaders/index.ts @@ -1,3 +1,4 @@ export * from './ContentLoader'; export * from './ProgressBarLoader'; export * from './ScreenLoader'; +export * from './LoaderTransparant'; diff --git a/src/config/api.config.ts b/src/config/api.config.ts index 535dc2a..81e8652 100644 --- a/src/config/api.config.ts +++ b/src/config/api.config.ts @@ -4,7 +4,9 @@ interface apiConfigProps { service_master_data: string; service_transaction: string; service_wallet: string; - transaction: string + transaction: string; + nationality: string; + service_disbursement: string; } const API_URL = import.meta.env.VITE_APP_API_URL; @@ -16,7 +18,10 @@ const apiConfig: apiConfigProps = { service_master_data: `${API_URL}/t`, service_transaction: `${API_URL}/tt`, service_wallet: `${API_URL}/w`, - transaction: `${API_URL}/x` + transaction: `${API_URL}/x`, + service_disbursement: `${API_URL}/s`, + nationality: `https://tpay.shiblysolution.id/cms/api/mobile/list-country/ +` }; export { apiConfig }; diff --git a/src/hooks/useCallApi.ts b/src/hooks/useCallApi.ts index a359418..c610dd0 100644 --- a/src/hooks/useCallApi.ts +++ b/src/hooks/useCallApi.ts @@ -57,6 +57,24 @@ const useCallApi = () => { } }, []); + const PostDataFile = useCallback(async (url: string, field: any, config = {}) => { + try { + const response = await axios.post(url, field, config); + const result = response.data; + + if (result.status) { + return { status: true, message: result.data }; + } + return { status: false, message: result.message }; + } catch (error: any) { + const { response } = error; + return { + status: false, + message: response?.data?.error ?? response?.data?.message ?? 'Something went wrong' + }; + } + }, []); + const PutData = useCallback(async (url: string, field: any) => { try { const response = await axios.put(url, field); @@ -85,7 +103,7 @@ const useCallApi = () => { } }, []); - return { GetData, PostData, PutData, DeleteData, GetExportData }; + return { GetData, PostData, PutData, DeleteData, GetExportData, PostDataFile }; }; export { useCallApi }; diff --git a/src/pages/disbursement/history-transaction/HistoryTransaction.tsx b/src/pages/disbursement/history-transaction/HistoryTransaction.tsx new file mode 100644 index 0000000..61b9d2d --- /dev/null +++ b/src/pages/disbursement/history-transaction/HistoryTransaction.tsx @@ -0,0 +1,39 @@ +import { Container, DataGridInner } from '@/components'; +import { TransactionProvider } from './hooks/TransactionContext'; +import { Breadcrumbs, Link } from '@mui/material'; +import { Helmet } from 'react-helmet'; +import { UploadBatchDialog } from './blocks/UploadBatchDialog'; + +const HistoryTransactionDisbursement = () => { + return ( + <> + + TPAY | History Disbursement + + + +

DISBURSEMENT

+ + + Dashboard + + + + Disbursement + + + + History Disbursement + + +
+ +
+ +
+
+ + ); +}; + +export default HistoryTransactionDisbursement; diff --git a/src/pages/disbursement/history-transaction/blocks/DetailTransaction.tsx b/src/pages/disbursement/history-transaction/blocks/DetailTransaction.tsx new file mode 100644 index 0000000..28df134 --- /dev/null +++ b/src/pages/disbursement/history-transaction/blocks/DetailTransaction.tsx @@ -0,0 +1,137 @@ +import { useTransactionContext } from '../hooks/useTransactionContext'; +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.service_disbursement; + +type StatusCode = 'W' | 'P' | 'F' | 'D' | 'Y'; + +interface StatusInfo { + label: string; + bg: string; + text: string; +} + +const statusMap: Record = { + W: { label: 'Waiting', bg: 'bg-yellow-100', text: 'text-yellow-600' }, + P: { label: 'Pending', bg: 'bg-blue-100', text: 'text-blue-600' }, + F: { label: 'Fail', bg: 'bg-red-100', text: 'text-red-600' }, + D: { label: 'Done', bg: 'bg-green-100', text: 'text-green-600' }, + Y: { label: 'Active', 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 DetailTransaction = () => { + const { GetData } = useCallApi(); + const { + showDetailDialog, + setShowDetailDialog, + selectedTransactionId + } = useTransactionContext(); + + const [transactionDetails, setTransactionDetails] = useState(null); + + useEffect(() => { + const fetchTransactionDetails = async () => { + console.log(selectedTransactionId) + if (selectedTransactionId) { + try { + const response = await GetData(`${API_URL}/transaction/history/${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 + + + {/* Tab Content */} +
+
+
+ + + + + + + + + + + + + + + {transactionDetails?.log && transactionDetails?.log.length > 0 ? ( + transactionDetails.log.map((log: { customer: any, amount: number, remark: string, reference: string, response_date: string, payment_response: string, status: string}, index: number) => ( + + + + + + + + + + + )) + ) : ( + + + + )} + +
UsernameNameTransfer AmountDescriptionInvoice NumberProcess DateResponseStatus
{log.customer.username ?? '-'}{log.customer.fullname ?? '-'}{log.amount ?? '-'}{log.remark ?? '-'}{log.reference ?? '-'}{log.response_date ?? '-'}{log.payment_response ?? '-'}{renderStatusBadge(log.status) ?? '-'}
+ No logs available +
+
+
+ +
+
+
+
+ ); +}; + +export default DetailTransaction; diff --git a/src/pages/disbursement/history-transaction/blocks/ListToolbar.tsx b/src/pages/disbursement/history-transaction/blocks/ListToolbar.tsx new file mode 100644 index 0000000..4b6c69f --- /dev/null +++ b/src/pages/disbursement/history-transaction/blocks/ListToolbar.tsx @@ -0,0 +1,70 @@ +import { DefaultTooltip, KeenIcon, useDataGrid } from '@/components'; +import { useTransactionContext } from '../hooks/useTransactionContext'; +import { Button } from '@/components/ui/button'; +import { useCallback, useState, useEffect } from 'react'; +import { toast } from 'sonner'; + +const ListToolbar = () => { + const { table, reload } = useDataGrid(); + const { handleUploadBatchDialog } = useTransactionContext(); + + // 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/disbursement/history-transaction/blocks/UploadBatchDialog.tsx b/src/pages/disbursement/history-transaction/blocks/UploadBatchDialog.tsx new file mode 100644 index 0000000..237a72e --- /dev/null +++ b/src/pages/disbursement/history-transaction/blocks/UploadBatchDialog.tsx @@ -0,0 +1,194 @@ +import { MouseEvent, useCallback, useEffect, useRef, useState } from 'react'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from '@/components/ui/select'; + +import { + Dialog, + DialogBody, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle +} from '@/components/ui/dialog'; +import { useTransactionContext } from '../hooks'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import { apiConfig } from '@/config/api.config'; +import { Alert, KeenIcon, useDataGrid } from '@/components'; +import { toast } from 'sonner'; +import { useCallApi } from '@/hooks'; +import { doSaveLogActivity } from '@/actions/GlobalActions'; +import clsx from 'clsx'; + +const API_URL = apiConfig.service_disbursement; + +const UploadBatchDialog = () => { + const parentRef = useRef(null); + const { showUploadBatchDialog, handleUploadBatchDialog } = useTransactionContext(); + const { reload } = useDataGrid(); + const { PostData, PostDataFile, GetData } = useCallApi(); + const [alert, setAlert] = useState({ + show: false, + message: '' + }); + const initialState: { + execution_date: string, + file: File | null; + } = { + execution_date: '', + file: null + } + const [formField, setFormField] = useState(initialState); + + const resetForm = () => { + setFormField(initialState); + setAlert({ show: false, message: '' }); + }; + + /* actions */ + const doUploadBatch = useCallback( + async (e: React.FormEvent) => { + e.preventDefault(); + + const formData = new FormData(); + formData.append('execution_date', formField.execution_date); + if (formField.file) { + formData.append('file', formField.file); + } + + try { + const response = await PostDataFile(`${API_URL}/upload-excel`, formData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }); + + if (response?.status) { + handleUploadBatchDialog(false); + resetForm(); + reload(); + + const createActivity = { + module: 'Disbursement', + description: `Create New Disbursement => ${formField.file?.name}`, + action: 'C' + }; + + doSaveLogActivity(createActivity); + toast.success('Success Create Disbursement'); + } else { + toast.error('Failed to create disbursement'); + setAlert({ show: true, message: response?.message ?? 'Failed to create disbursement.' }); + } + } catch (error) { + toast.error('Error uploading batch'); + setAlert({ show: true, message: 'Something went wrong. Please try again.' }); + } + }, + [formField] + ); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + console.log('Form data before submit:', formField); + + if ( + formField.execution_date.trim() === '' || + formField.file === null + ) { + setAlert({ show: true, message: 'Please fill in all required fields.' }); + return; + } + + doUploadBatch(e); + // console.log(formField); + setAlert({ show: false, message: '' }); + }; + + useEffect(() => { + if (showUploadBatchDialog === false) { + resetForm(); + } + }, [showUploadBatchDialog]); + + return ( + handleUploadBatchDialog(open)}> + + + + +
+
+

Upload Batch

+
+
+
{ + handleUploadBatchDialog(false); + resetForm(); + }} + > + +
+
+
+ +
+ {alert.show && ( + +

{alert.message}

+
+ )} +
+
+
+
+ + + setFormField((prev) => ({ ...prev, execution_date: target.value })) + } + /> +
+
+
+
+ + + setFormField((prev) => ({ ...prev, file: target.files?.[0] ?? null })) + } + /> +
+
+
+ +
+
+
+
+
+
+
+ ); +}; + +export { UploadBatchDialog }; diff --git a/src/pages/disbursement/history-transaction/hooks/TransactionContext.tsx b/src/pages/disbursement/history-transaction/hooks/TransactionContext.tsx new file mode 100644 index 0000000..19e2382 --- /dev/null +++ b/src/pages/disbursement/history-transaction/hooks/TransactionContext.tsx @@ -0,0 +1,289 @@ +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 moment from 'moment'; +import DetailTransaction from '../blocks/DetailTransaction'; + +interface TransactionProps { + id: number; + name: string; +} + +interface ContextProps { + getTransactionLists: ( + limit: number, + page: number, + with_deleted: boolean, + order_field: any, + order_direction: any, + filter: any + ) => Promise<{ data: TransactionProps[]; totalCount: number } | undefined>; + showDetailDialog: boolean; + setShowDetailDialog: React.Dispatch>; + selectedTransactionId: number | null; + setSelectedTransactionId: React.Dispatch>; + showUploadBatchDialog: boolean; + handleUploadBatchDialog: (show: boolean) => void; +} + +const initialProps: ContextProps = { + getTransactionLists: async () => ({ data: [], totalCount: 0 }), + showDetailDialog: false, + setShowDetailDialog: () => { }, + selectedTransactionId: null, + setSelectedTransactionId: () => { }, + showUploadBatchDialog: false, + handleUploadBatchDialog: (show: boolean) => {}, +}; + +type StatusCode = 'W' | 'P' | 'F' | 'D' | 'Y'; + +interface StatusInfo { + label: string; + bg: string; + text: string; +} + +const statusMap: Record = { + W: { label: 'Waiting', bg: 'bg-yellow-100', text: 'text-yellow-600' }, + P: { label: 'Pending', bg: 'bg-blue-100', text: 'text-blue-600' }, + F: { label: 'Fail', bg: 'bg-red-100', text: 'text-red-600' }, + D: { label: 'Done', bg: 'bg-green-100', text: 'text-green-600' }, + Y: { label: 'Active', bg: 'bg-green-100', text: 'text-green-600' }, +}; + + +const ManageTransactionContext = createContext(initialProps); +const API_URL = apiConfig.service_disbursement; + +const TransactionProvider = ({ children }: { children: React.ReactNode }) => { + const [showDetailDialog, setShowDetailDialog] = useState(false); + const [selectedTransactionId, setSelectedTransactionId] = useState(null); + const [showUploadBatchDialog, setShowUploadBatchDialog] = useState(false); + const [transaction, setTransaction] = useState([]); + const { GetData } = useCallApi(); + const navigate = useNavigate(); + + const handleUploadBatchDialog = useCallback((show: boolean) => { + setShowUploadBatchDialog(show); + }, []); + + const columns = useMemo[]>( + () => [ + // { + // accessorKey: 'transaction_date', + // header: ({ column }) => , + // enableSorting: false, + // enableHiding: false, + // meta: { + // headerClassName: 'w-[250px]' + // } + // }, + { + accessorKey: 'file_name', + header: ({ column }) => , + enableSorting: false, + enableHiding: false, + meta: { + headerClassName: 'w-[250px]' + }, + }, + { + accessorFn: (row) => { + return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(row.amount); + }, + id: 'amount', + header: ({ column }) => , + enableSorting: false, + enableHiding: false, + meta: { + headerClassName: 'w-[250px]', + }, + }, + { + accessorKey: 'total_record', + header: ({ column }) => , + enableSorting: false, + enableHiding: false, + meta: { + headerClassName: 'w-[250px]' + }, + }, + { + accessorKey: 'total_success', + header: ({ column }) => , + enableSorting: false, + enableHiding: false, + meta: { + headerClassName: 'w-[250px]' + }, + }, + { + accessorKey: 'total_fail', + header: ({ column }) => , + enableSorting: false, + enableHiding: false, + meta: { + headerClassName: 'w-[250px]' + }, + }, + { + accessorKey: 'total_pending', + header: ({ column }) => , + enableSorting: false, + enableHiding: false, + meta: { + headerClassName: 'w-[250px]' + }, + }, + { + accessorFn: (row) => row.status, + id: 'status', + header: ({ column }) => , + enableSorting: false, + enableHiding: false, + cell: ({ row }) => { + const status = row.original.status as StatusCode; + const { label, bg, text } = statusMap[status] ?? { + label: 'Unknown', + bg: 'bg-gray-100', + text: 'text-gray-600', + }; + + return ( + + {label} + + ); + }, + meta: { + headerClassName: 'w-[100px]', + cellClassName: 'text-center', + }, + }, + { + accessorFn: (row) => row.execution_date, + id: 'execution_date', + header: ({ column }) => , + enableSorting: true, + enableHiding: false, + cell: ({ row }) => moment(row.original.execution_date).format('YYYY-MM-DD HH:mm:ss') + }, + { + accessorFn: (row) => row.done_date, + id: 'done_date', + header: ({ column }) => , + enableSorting: true, + enableHiding: false, + cell: ({ row }) => row.original.done_date ? moment(row.original.done_date).format('YYYY-MM-DD HH:mm:ss') : '' + }, + { + 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 = { + + }; + + const response = await GetData(`${API_URL}/transaction/history`, { + limit, + page: page + 1, + with_deleted: false, + order_field: "id", + 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 { TransactionProvider, ManageTransactionContext }; +export type { TransactionProps }; diff --git a/src/pages/disbursement/history-transaction/hooks/index.tsx b/src/pages/disbursement/history-transaction/hooks/index.tsx new file mode 100644 index 0000000..d25a2f6 --- /dev/null +++ b/src/pages/disbursement/history-transaction/hooks/index.tsx @@ -0,0 +1,2 @@ +export * from './TransactionContext'; +export * from './useTransactionContext'; diff --git a/src/pages/disbursement/history-transaction/hooks/useTransactionContext.tsx b/src/pages/disbursement/history-transaction/hooks/useTransactionContext.tsx new file mode 100644 index 0000000..4af7036 --- /dev/null +++ b/src/pages/disbursement/history-transaction/hooks/useTransactionContext.tsx @@ -0,0 +1,12 @@ +import { useContext } from 'react'; +import { ManageTransactionContext } from './TransactionContext'; + +const useTransactionContext = () => { + const context = useContext(ManageTransactionContext); + + if (!context) throw new Error('useTransactionContext must be used within AuthProvider'); + + return context; +}; + +export { useTransactionContext }; diff --git a/src/pages/master/products/blocks/EditDialog.tsx b/src/pages/master/products/blocks/EditDialog.tsx index d16ae0e..493489d 100644 --- a/src/pages/master/products/blocks/EditDialog.tsx +++ b/src/pages/master/products/blocks/EditDialog.tsx @@ -23,6 +23,7 @@ import { SelectValue } from '@/components/ui/select'; import { doSaveLogActivity } from '@/actions/GlobalActions'; +import { NumericFormat } from 'react-number-format'; interface ProviderProps { provider_id: string; @@ -41,15 +42,29 @@ const EditDialog = () => { show: false, message: '' }); - const initialState = { + const initialState: { + name: string; + code: string; + type: string; + description: string; + price_point: string | number | null; + price_cash: string | number | null; + cashback_point: string | number | null; + cashback_cash: string | number | null; + status: string; + provider: string; + process_on_third_party: string; + updated_by: string; + updated_at: string; + } = { name: '', code: '', type: '', description: '', - price_point: 0, - price_cash: 0, - cashback_point: 0, - cashback_cash: 0, + price_point: null, + price_cash: null, + cashback_point: null, + cashback_cash: null, status: '', provider: '', process_on_third_party: '', @@ -139,11 +154,13 @@ const EditDialog = () => { formField.type.trim() === '' || formField.code.trim() === '' || formField.description.trim() === '' || - formField.price_point === 0 || - formField.price_cash === 0 || - formField.cashback_point === 0 || - formField.cashback_cash === 0 || - formField.status === '' || + formField.price_point === null || + formField.price_cash === null || + formField.cashback_point === null || + formField.cashback_cash === null || + formField.status.trim() === '' || + formField.provider.trim() === '' || + formField.process_on_third_party.trim() === '' || formField.updated_by.trim() === '' || formField.updated_at.trim() === '' ) { @@ -152,7 +169,7 @@ const EditDialog = () => { } doUpdateProduct(e); - console.log(formField); + // console.log(formField); setAlert({ show: false, message: '' }); }; @@ -260,16 +277,19 @@ const EditDialog = () => { - { - const value = parseFloat(e.target.value); - setFormField({ ...formField, price_point: isNaN(value) ? 0 : value }); + value={formField.price_point ?? ''} + thousandSeparator="." + decimalSeparator="," + allowNegative={false} + onValueChange={(values) => { + setFormField((prev) => ({ + ...prev, + price_point: values.floatValue !== undefined ? values.floatValue : '' + })); }} + placeholder="Enter Price Point" /> @@ -279,16 +299,19 @@ const EditDialog = () => { - { - const value = parseFloat(e.target.value); - setFormField({ ...formField, price_cash: isNaN(value) ? 0 : value }); + value={formField.price_cash ?? ''} + thousandSeparator="." + decimalSeparator="," + allowNegative={false} + onValueChange={(values) => { + setFormField((prev) => ({ + ...prev, + price_cash: values.floatValue !== undefined ? values.floatValue : '' + })); }} + placeholder="Enter Price Cash" /> @@ -298,16 +321,19 @@ const EditDialog = () => { - { - const value = parseFloat(e.target.value); - setFormField({ ...formField, cashback_point: isNaN(value) ? 0 : value }); + value={formField.cashback_point ?? ''} + thousandSeparator="." + decimalSeparator="," + allowNegative={false} + onValueChange={(values) => { + setFormField((prev) => ({ + ...prev, + cashback_point: values.floatValue !== undefined ? values.floatValue : '' + })); }} + placeholder="Enter Cashback Point" /> @@ -317,16 +343,19 @@ const EditDialog = () => { - { - const value = parseFloat(e.target.value); - setFormField({ ...formField, cashback_cash: isNaN(value) ? 0 : value }); + value={formField.cashback_cash ?? ''} + thousandSeparator="." + decimalSeparator="," + allowNegative={false} + onValueChange={(values) => { + setFormField((prev) => ({ + ...prev, + cashback_cash: values.floatValue !== undefined ? values.floatValue : '' + })); }} + placeholder="Enter Cashback Cash" /> diff --git a/src/pages/master/products/hooks/ManageProductsContext.tsx b/src/pages/master/products/hooks/ManageProductsContext.tsx index 541c710..236e306 100644 --- a/src/pages/master/products/hooks/ManageProductsContext.tsx +++ b/src/pages/master/products/hooks/ManageProductsContext.tsx @@ -72,6 +72,19 @@ const ManageProductsContextProvider = ({ children }: { children: React.ReactNode const columns = useMemo[]>( () => [ + { + accessorFn: (row) => row.code, + id: 'code', + header: ({ column }) => , + enableSorting: false, + enableHiding: false, + meta: { + headerClassName: 'w-[100px]' + }, + filterFn: (row, id, value) => { + return row.original.products_name.toLowerCase().includes(value.toLowerCase()); + } + }, { accessorFn: (row) => row.name, id: 'name', @@ -89,12 +102,32 @@ const ManageProductsContextProvider = ({ children }: { children: React.ReactNode accessorFn: (row) => row.description, id: 'description', header: ({ column }) => , - enableSorting: true, + enableSorting: false, enableHiding: false, meta: { headerClassName: 'w-[250px]' } }, + { + accessorFn: (row) => row.provider?.name, + id: 'provider_name', + header: ({ column }) => , + enableSorting: false, + enableHiding: false, + meta: { + headerClassName: 'w-[250px]' + } + }, + { + accessorFn: (row) => row.type, + id: 'type', + header: ({ column }) => , + enableSorting: false, + enableHiding: false, + meta: { + headerClassName: 'w-[150px]' + } + }, { accessorFn: (row) => row.price_point, id: 'price_point', @@ -115,6 +148,64 @@ const ManageProductsContextProvider = ({ children }: { children: React.ReactNode headerClassName: 'w-[100px]' } }, + { + accessorFn: (row) => row.cashback_point, + id: 'cashback_point', + header: ({ column }) => , + enableSorting: true, + enableHiding: false, + meta: { + headerClassName: 'w-[100px]' + } + }, + { + accessorFn: (row) => row.cashback_cash, + id: 'cashback_cash', + header: ({ column }) => , + enableSorting: true, + enableHiding: false, + meta: { + headerClassName: 'w-[100px]' + } + }, + { + accessorFn: (row) => row.process_on_third_party, + id: 'process_on_third_party', + header: ({ column }) => , + cell: ({ row }) => { + const isActive = row.original.process_on_third_party === 'Y'; + + return {isActive ? 'Yes' : 'No'}; + }, + enableSorting: true, + enableHiding: false, + meta: { + headerClassName: 'w-[100px]' + } + }, + { + accessorFn: (row) => row.status, + id: 'status', + header: ({ column }) => , + cell: ({ row }) => { + const isActive = row.original.status === 'Y'; + + return ( + + {isActive ? 'Active' : 'Inactive'} + + ); + }, + enableSorting: true, + enableHiding: false, + meta: { + headerClassName: 'w-[100px]' + } + }, { id: 'actions', header: ({ column }) => , @@ -160,7 +251,7 @@ const ManageProductsContextProvider = ({ children }: { children: React.ReactNode order_direction: sorting[0].desc == false ? 'ASC' : 'DESC', filter: JSON.stringify(filter) }); - // console.log(response?.data); + console.log(response?.data); setProducts(response?.data.list); return { data: response?.data.list, totalCount: response?.data.total_count }; } catch (error) { @@ -189,7 +280,7 @@ const ManageProductsContextProvider = ({ children }: { children: React.ReactNode pagination={{ size: 10 }} toolbar={} layout={{ card: true }} - sorting={[{ id: 'Product.id', desc: false }]} + sorting={[{ id: 'Product.created_at', desc: true }]} serverSide={true} onFetchData={({ pageIndex, pageSize, sorting, columnFilters }) => getProductsLists(pageIndex, pageSize, sorting, columnFilters) diff --git a/src/pages/members/kyc/Kyc.tsx b/src/pages/members/kyc/Kyc.tsx index 3aa6c6b..65c7215 100644 --- a/src/pages/members/kyc/Kyc.tsx +++ b/src/pages/members/kyc/Kyc.tsx @@ -5,12 +5,15 @@ import React, { createContext, useContext, useState, useEffect } from 'react'; import { ManageKycContextProvider } from './hooks'; import { columns, initialMember } from './Columns'; import { useAuthContext } from '@/auth'; +import { LoaderTransparant } from '@/components'; import { apiConfig } from '@/config/api.config'; import ConfirmDialog from '@/components/confirm'; import axios from 'axios'; import { toast } from 'sonner'; const BASE_URL = apiConfig.service_customer; -import CustomerDialog from '../manage-members/CustomerDetailModal'; +const BASE_URL_MASTER_DATA = apiConfig.service_master_data; +// import CustomerDialog from '../manage-members/CustomerDetailModal'; +import DetailMember from '../manage-members/blocks/DetailMember'; import { Breadcrumbs, Link } from '@mui/material'; import { Helmet } from 'react-helmet'; @@ -37,6 +40,7 @@ const Kyc = () => { const [loading, setLoading] = useState(false); const [members, setMembers] = useState([]); const [member, setMember] = useState(initialMember); + const [profession, setProfession] = useState([]); const [isDialogOpen, setIsDialogOpen] = useState(false); const [dialogOpen, setDialogOpen] = useState(false); const [dialogType, setDialogType] = useState(''); @@ -44,12 +48,12 @@ const Kyc = () => { const { getUser } = useAuthContext(); useEffect(() => { - fetchGroups(); + fetchCustomers(); }, []); - async function fetchGroups() { + async function fetchCustomers() { try { - let groups = await axios.get(`${BASE_URL}/customer/list`, { + let customers = await axios.get(`${BASE_URL}/customer/list`, { params: { limit: 10, page: 1, @@ -60,12 +64,22 @@ const Kyc = () => { } }); let temp = 1; - let resMembers = groups.data.data.list.map((el: any) => { + let resMembers = customers.data.data.list.map((el: any) => { el.no = temp++; el.name = el.fullname; return el; }); setMembers(resMembers); + let getProfession: any = await axios.get(`${BASE_URL_MASTER_DATA}/profession/list`, { + params: { + limit: 50, + page: 1, + with_deleted: false, + order_field: 'name', + order_direction: 'ASC', + } + }); + setProfession(getProfession.data.data.list) } catch (error: any) { toast.error(error.message); console.log(error); @@ -90,13 +104,19 @@ const Kyc = () => { } const handleYes = async () => { + setLoading(true) const userLogin: any = await getUser(); const updateData: any = member; const customerId = member.id; const description = member.description; + const destinationGroup = updateData.destinationGroup; updateData.updated_by = userLogin.data ? userLogin.data.id : ''; let today = new Date(); updateData.updated_at = today.toString(); + updateData.municipio = updateData.municipio_id; + updateData.posto_adms = updateData.posto_adms_id; + updateData.suco = updateData.suco_id; + updateData.aldeia = updateData.aldeia_id; delete updateData.id; delete updateData.pin; delete updateData.name; @@ -107,6 +127,14 @@ const Kyc = () => { delete updateData.destinationGroup; delete updateData.statusApproval; delete updateData.description; + delete updateData.municipio_id; + delete updateData.municipio_name; + delete updateData.posto_adms_id; + delete updateData.posto_adms_name; + delete updateData.suco_id; + delete updateData.suco_name; + delete updateData.aldeia_id; + delete updateData.aldeia_name; delete updateData.group_id; delete updateData.group_name; delete updateData.group_description; @@ -124,27 +152,32 @@ const Kyc = () => { if (updateData[property]) form.append(property, updateData[property]); } if (dialogType === 'reject') { - await axios.post(`${BASE_URL}/customer/reject`, { customerid: customerId }); + if (destinationGroup === "Premium") await axios.post(`${BASE_URL}/customer/reject`, { customerid: customerId, description: updateData.approval_description_premium }); + if (destinationGroup === "Agent") await axios.post(`${BASE_URL}/customer/reject`, { customerid: customerId, description: updateData.approval_description_agent }); } if (dialogType === 'update') { await axios.put(`${BASE_URL}/customer/update/${customerId}`, form); - if (updateData.isneedapproval == 1) - await axios.post(`${BASE_URL}/customer/approve`, { - customerid: customerId, - description: description - }); + if (updateData.isneedapproval == 1) await axios.post(`${BASE_URL}/customer/approve`, { customerid: customerId, description: description}); } - await fetchGroups(); + await fetchCustomers(); setDialogOpen(false); setIsDialogOpen(false); - toast.success('Success Update Kyc Member'); + toast.success(`Success Update & ${dialogType} Kyc Member`); } catch (error: any) { setDialogOpen(false); setIsDialogOpen(false); toast.error(error.message); + } finally { + setLoading(false) } }; + function setShowAddDialog(el: any) { + setIsDialogOpen(el) + } + + if (loading) return ; + return ( <> @@ -161,12 +194,23 @@ const Kyc = () => { onNo={() => setDialogOpen(false)} /> { member.id ? ( - + ): ""} diff --git a/src/pages/members/manage-members/Columns.tsx b/src/pages/members/manage-members/Columns.tsx index e657553..6c83c0b 100644 --- a/src/pages/members/manage-members/Columns.tsx +++ b/src/pages/members/manage-members/Columns.tsx @@ -178,5 +178,7 @@ export const initialMember = { posto_adms: '', suco: '', aldeia: '', - profession: '' + profession: '', + approval_description_premium: '', + approval_description_agent: '' }; diff --git a/src/pages/members/manage-members/CustomerDetailModal.tsx b/src/pages/members/manage-members/CustomerDetailModal.tsx index 152e749..e7c2110 100644 --- a/src/pages/members/manage-members/CustomerDetailModal.tsx +++ b/src/pages/members/manage-members/CustomerDetailModal.tsx @@ -295,8 +295,7 @@ const CustomerDialog = ({ open, handleClose, handleSubmit, initialData, viewStat {/* {getAdmAccess(page, formData, handleClose, fetchCustomers)} */} - { - page === 'kyc' ? ( + { page === 'kyc' ? ( <> Approval @@ -589,23 +588,21 @@ function showCustomerWallet(customerid: any) { }} > - - Name - + Name {item.wallet.name} - - Description - + Description {item.wallet.description} - - Transaction Today - + Balance + {item.amount} + + + Transaction Today {item.transaction_number_today} diff --git a/src/pages/members/manage-members/ManageMembers.tsx b/src/pages/members/manage-members/ManageMembers.tsx index dc82642..6be4b05 100644 --- a/src/pages/members/manage-members/ManageMembers.tsx +++ b/src/pages/members/manage-members/ManageMembers.tsx @@ -3,20 +3,22 @@ import { apiConfig } from '@/config/api.config'; import { columns, Members, initialMember } from './Columns'; import { useState, useEffect } from 'react'; import axios from 'axios'; -import CustomerDialog from './CustomerDetailModal'; +// import CustomerDialog from './CustomerDetailModal'; +import DetailMember from './blocks/DetailMember'; import ConfirmDialog from '@/components/confirm'; import { useAuthContext } from '@/auth'; -import { ScreenLoader } from '@/components'; +import { LoaderTransparant } from '@/components'; import { toast } from 'sonner'; import { Breadcrumbs, Link } from '@mui/material'; +const BASE_URL_MASTER_DATA = apiConfig.service_master_data; const BASE_URL = apiConfig.service_customer; import { Helmet } from 'react-helmet'; - const ManageMembers = () => { const [loading, setLoading] = useState(false); const [members, setMembers] = useState([]); const [selectedMember, setSelectedMember] = useState(''); const [member, setMember] = useState(initialMember); + const [profession, setProfession] = useState([]); const [isDialogOpen, setIsDialogOpen] = useState(false); const [dialogOpen, setDialogOpen] = useState(false); const [dialogType, setDialogType] = useState(''); @@ -34,7 +36,7 @@ const ManageMembers = () => { async function fetchCustomers() { try { - let groups = await axios.get(`${BASE_URL}/customer/list`, { + let customers = await axios.get(`${BASE_URL}/customer/list`, { params: { limit: 20, page: 1, @@ -44,12 +46,22 @@ const ManageMembers = () => { } }); let temp = 1; - let resMembers = groups.data.data.list.map((el: any) => { + let resMembers = customers.data.data.list.map((el: any) => { el.no = temp++; el.name = el.fullname; return el; }); setMembers(resMembers); + let getProfession: any = await axios.get(`${BASE_URL_MASTER_DATA}/profession/list`, { + params: { + limit: 50, + page: 1, + with_deleted: false, + order_field: 'name', + order_direction: 'ASC', + } + }); + setProfession(getProfession.data.data.list) } catch (error: any) { toast.error(error.message); console.log(error); @@ -75,11 +87,16 @@ const ManageMembers = () => { } const handleYes = async () => { + setLoading(true) const userLogin: any = await getUser(); const updateData: any = member; updateData.updated_by = userLogin.data ? userLogin.data.id : ''; let today = new Date(); updateData.updated_at = today.toString(); + updateData.municipio = updateData.municipio_id; + updateData.posto_adms = updateData.posto_adms_id; + updateData.suco = updateData.suco_id; + updateData.aldeia = updateData.aldeia_id; delete updateData.id; delete updateData.pin; delete updateData.name; @@ -90,6 +107,14 @@ const ManageMembers = () => { delete updateData.destinationGroup; delete updateData.statusApproval; delete updateData.description; + delete updateData.municipio_id; + delete updateData.municipio_name; + delete updateData.posto_adms_id; + delete updateData.posto_adms_name; + delete updateData.suco_id; + delete updateData.suco_name; + delete updateData.aldeia_id; + delete updateData.aldeia_name; delete updateData.group_id; delete updateData.group_name; delete updateData.group_description; @@ -113,18 +138,22 @@ const ManageMembers = () => { } }); // if (dialogType === 'create') await axios.post(`${BASE_URL}/customers/create`, member) - await fetchCustomers(); - setDialogOpen(false); - setIsDialogOpen(false); toast.success('Success Update Member'); } catch (error: any) { - setDialogOpen(false); - setIsDialogOpen(false); toast.error(error.message); + } finally { + setDialogOpen(false); + closeDialog(); + await fetchCustomers(); + setLoading(false) } }; - if (loading) return ; + function setShowAddDialog(el: any) { + setIsDialogOpen(el) + } + + if (loading) return ; return ( <> @@ -141,14 +170,16 @@ const ManageMembers = () => { onYes={handleYes} onNo={() => setDialogOpen(false)} /> - { member.id ? ( - + { member.id !== '' ? ( + ): ""}

Manage Members

diff --git a/src/pages/members/manage-members/blocks/AdmAccess.tsx b/src/pages/members/manage-members/blocks/AdmAccess.tsx new file mode 100644 index 0000000..735e832 --- /dev/null +++ b/src/pages/members/manage-members/blocks/AdmAccess.tsx @@ -0,0 +1,248 @@ +import { useState, useEffect } from 'react'; +import axios from 'axios'; +import { toast } from 'sonner'; +import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle +} from '@/components/ui/dialog'; +import { + Select, + SelectTrigger, + SelectContent, + SelectItem, + SelectValue +} from '@/components/ui/select'; +import { apiConfig } from '@/config/api.config'; +import ConfirmDialog from '@/components/confirm'; +const BASE_URL_CUSTOMER = apiConfig.service_customer; + +// ACCESS ADM +export default function AdmAccess( + page: string, + data: any, + handleClose: any, + fetchCustomers: any, + viewOnly: any, + setViewOnly: any +) { + const [dialogOpen, setDialogOpen] = useState(false); + const [dialogType, setDialogType] = useState(''); + const [changeGroup, setChangeGroup] = useState(''); + const [changeGroupD, setChangeGroupD] = useState(false); + const [groups, setGroups] = useState([]); + + useEffect(() => { + fetchGroups(); + }, []); + + const fetchGroups = async () => { + try { + let getGroups = await axios.get(`${BASE_URL_CUSTOMER}/groups/list`, { + params: { + limit: 50, + page: 1, + with_deleted: false, + order_field: 'name', + order_direction: 'ASC' + } + }); + setGroups(getGroups.data.data.list); + } catch (error: any) { + toast.error(error.message); + } + }; + + const handleYes = async () => { + try { + if (dialogType === 'update status') { + let statusNext = getPinStatus(data.status).res; + if (statusNext) + await axios.put(`${BASE_URL_CUSTOMER}/customer/statuspin`, { + customerid: data.id, + status: statusNext + }); + else toast.error('Handle Active/Suspend only'); + await fetchCustomers(); + toast.success('Success Update Status'); + } + if (dialogType === 'reset pin') { + if (data.id) + await axios.post(`${BASE_URL_CUSTOMER}/customer/resetpin`, { customerid: data.id }); + else throw { message: 'data.id not found' }; + await fetchCustomers(); + toast.success('Pin will send to customer MSISDN'); + } + } catch (error: any) { + toast.error(error.message); + } finally { + setDialogOpen(false); + handleClose(); + } + }; + + function buttonStatus(e: any) { + e.preventDefault(); + setDialogType('update status'); + setDialogOpen(true); + } + + function buttonResetPin(e: any) { + e.preventDefault(); + setDialogType('reset pin'); + setDialogOpen(true); + } + + async function buttonChangeGroup() { + try { + let dataObj = { + customerid: data.id, + destination_group: changeGroup + }; + if (data.group_id === changeGroup) + throw { message: `You update same group as the exist customer group` }; + if (dataObj.customerid && dataObj.destination_group) { + await axios.post(`${BASE_URL_CUSTOMER}/customer/change-group`, dataObj); + } + toast.success('Success Change group'); + await fetchCustomers(); + setChangeGroupD(false); + handleClose(); + } catch (error: any) { + console.log(error); + toast.error(error.message); + } + } + + function openChangeGroupDialog(e: any) { + e.preventDefault(); + setChangeGroup(data.group_id); + setChangeGroupD(true); + } + + function btnConfirmDialog(status: boolean) { + setDialogOpen(status); + } + + if (page !== 'kyc') { + return ( +
+

Access Administration

+ +
+ {/* Left Side */} +
+
+

Pin Status: {getPinStatus(data.status).msg}

+ +
+
+

Reset PIN

+ +
+
+ + {/* Right Side */} +
+
+

Change Group

+ +
+
+

Edit Member

+ +
+
+
+ + {/* Change Group Dialog */} + + + + + Are you sure to change customer Group? + + + +
+

Destination Group

+ +
+ + + + + +
+
+ btnConfirmDialog(false)} + onOpenChange={() => setDialogOpen(false)} + title="Confirm Action" + content={`Are you sure you want to ${dialogType}?`} + onYes={handleYes} + onNo={() => btnConfirmDialog(false)} + /> +
+ ); + } else { + return ''; + } +} + +function getPinStatus(status: string) { + if (status === 'Y') + return { + msg: 'Active', + btn: 'Block PIN', + res: 'Block' + }; + if (status === 'N') + return { + msg: 'Not Active', + btn: 'Activate PIN', + res: null + }; + if (status === 'P') + return { + msg: 'Suspend PIN', + btn: 'Unblock PIN', + res: 'UnBlock' + }; + if (status === 'O') + return { + msg: 'Suspend OTP', + btn: 'Unblock OTP', + res: null + }; + return { + msg: 'None', + btn: 'No status found', + res: null + }; +} diff --git a/src/pages/members/manage-members/blocks/CustomerWallet.tsx b/src/pages/members/manage-members/blocks/CustomerWallet.tsx new file mode 100644 index 0000000..11d4bbc --- /dev/null +++ b/src/pages/members/manage-members/blocks/CustomerWallet.tsx @@ -0,0 +1,69 @@ +import React, { useState, useEffect } from 'react'; +import axios from 'axios'; +import { Card } from '@/components/ui/card'; +import { Separator } from '@/components/ui/separator'; +import { apiConfig } from '@/config/api.config'; +import { toast } from 'sonner'; +const BASE_URL_CUSTOMER = apiConfig.service_customer; + +export default function CustomerWallet(customerid: any) { + const [customerWallet, setCustomerWallet] = useState([]); + if (!customerid) return ''; + + useEffect(() => { + fetchCustomerWallet(); + }, []); + + async function fetchCustomerWallet() { + try { + let getCustWallet = await axios.get(`${BASE_URL_CUSTOMER}/customer/wallet`, { + params: { customerid: customerid } + }); + setCustomerWallet(getCustWallet.data.data.data); + } catch (error: any) { + // toast.error(error.message); + toast.error(`Wallet Not Found`); + } + } + + return ( +
+

Wallet Member

+ + {customerWallet.length ? ( +
+ + + + + + + + + + + + {customerWallet.map((item:any, index) => ( + + + + + + + + ))} + +
NoNameBalanceMonth LimitCredit Limit
{index+1}{item.wallet}{item.amount}{item.monthly_limit}{item.credit_limit}
+
+ ) : ( +

No wallet

+ )} +
+
+ ); +} diff --git a/src/pages/members/manage-members/blocks/DetailMember.tsx b/src/pages/members/manage-members/blocks/DetailMember.tsx new file mode 100644 index 0000000..9853208 --- /dev/null +++ b/src/pages/members/manage-members/blocks/DetailMember.tsx @@ -0,0 +1,369 @@ +import { apiConfig } from '@/config/api.config'; +import { Alert, useDataGrid } from '@/components'; +import { useCallApi } from '@/hooks'; +import { getAuth } from '@/auth'; +import axios from 'axios'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { toast } from 'sonner'; +import { + Dialog, + DialogBody, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle +} from '@/components/ui/dialog'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from '@/components/ui/select'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +const BASE_URL_MASTER_DATA = apiConfig.service_master_data; +const URL_NATIONALITY = apiConfig.nationality; +import { initialMember } from "../Columns"; +import AdmAccess from './AdmAccess'; +import CustomerWallet from './CustomerWallet'; + +const DetailMember = ({ showAddDialog, setShowAddDialog, handleSubmit, initialData, handleReject, page, fetchCustomers, + handleClose, profession + }: any) => { + const [formData, setFormData] = useState(initialData || initialMember); + const [viewOnly, setViewOnly] = useState(false); + const [nationality, setNationality] = useState([]); + const [municipios, setMunicipios] = useState([]); + const [aldeias, setAldeias] = useState([]); + const [postoAdm, setPostoAdm] = useState([]); + const [sucos, setSucos] = useState([]); + const [genders] = useState([ { name: 'Male',id: 'M' }, { name: 'Female',id: 'F' }]) + const [status] = useState([ + { name: 'Active',id: 'Y' }, { name: 'Inactive',id: 'N' }, { name: 'Suspend PIN',id: 'P' }, { name: 'Suspend OTP',id: 'O' } + ]) + const [banks] = useState([ + { name: 'BNCTL',id: 'BNCTL' }, { name: 'BRI',id: 'BRI' }, { name: 'BNU',id: 'BNU' }, { name: 'Mandiri',id: 'Mandiri' } + ]) + const parentRef = useRef(null); + const [alert, setAlert] = useState({ + show: false, + message: '' + }); + + useEffect(() => { + setFormData(initialData || {}); + // fetchMasterData() + }, [initialData]); + + const handleChange = async (e: any) => { + const { name, value } = e.target; + if (name === "file_selfie" || name === "photouser" || name === 'file_document_id' || name === "file_document_id_selfie" || + name === "file_commercial_license") { // FOR FILE ONLY + setFormData({ ...formData, [name]: e.target.files[0] }); + } else if(name === "nationality") { + let getNationality = await axios.get(`${URL_NATIONALITY}/${value}`); + setNationality(getNationality.data.data) + setFormData({ ...formData, [name]: value }); + } else { + setFormData({ ...formData, [name]: value }); + if (name === 'municipio' || name === 'posto_adms' || name === 'suco') await getMasterAfter(name, value); + } + }; + + async function getMasterAfter(name: string, id: any) { + if (name === 'municipio') { + let getMunicipiosPosto = await axios.get(`${BASE_URL_MASTER_DATA}/municipios/postoadms/${id}`, { + params: { + limit: 50, + page: 1, + with_deleted: false, + order_field: 'name', + order_direction: 'ASC', + } + }); + setPostoAdm(getMunicipiosPosto.data.data) + } + if (name === 'posto_adms') { + let getPostoSuco = await axios.get(`${BASE_URL_MASTER_DATA}/postoadms/sucos/${id}`, { + params: { + limit: 50, + page: 1, + with_deleted: false, + order_field: 'name', + order_direction: 'ASC', + } + }); + setSucos(getPostoSuco.data.data) + } + if (name === 'suco') { + let getSucoAldeias = await axios.get(`${BASE_URL_MASTER_DATA}/sucos/aldeias/${id}`, { + params: { + limit: 50, + page: 1, + with_deleted: false, + order_field: 'name', + order_direction: 'ASC', + } + }); + setAldeias(getSucoAldeias.data.data) + } + } + + const handleAddDialog = (show:boolean) => { + if (show) { + setShowAddDialog(show) + } else { + handleClose() + setShowAddDialog(show) + } + } + + async function fetchMasterData() { + try { + let getMunicipios = await axios.get(`${BASE_URL_MASTER_DATA}/municipios/list`, { + params: { + limit: 50, + page: 1, + with_deleted: false, + order_field: 'name', + order_direction: 'ASC', + } + }); + setMunicipios(getMunicipios.data.data.list) + let getPostoAdms = await axios.get(`${BASE_URL_MASTER_DATA}/postoadms/list`, { + params: { + limit: 50, + page: 1, + with_deleted: false, + order_field: 'name', + order_direction: 'ASC', + } + }); + setPostoAdm(getPostoAdms.data.data.list) + let getSucos = await axios.get(`${BASE_URL_MASTER_DATA}/sucos/list`, { + params: { + limit: 50, + page: 1, + with_deleted: false, + order_field: 'name', + order_direction: 'ASC', + } + }); + setSucos(getSucos.data.data.list) + let getAldeias = await axios.get(`${BASE_URL_MASTER_DATA}/aldeias/list`, { + params: { + limit: 50, + page: 1, + with_deleted: false, + order_field: 'name', + order_direction: 'ASC', + } + }); + setAldeias(getAldeias.data.data.list) + } catch (error) { + console.log(error); + } + } + + function buttonOnSubmit(e:any) { + e.preventDefault(); + if (page === 'kyc' && formData.destinationGroup === "Premium" && !formData.approval_description_premium) return toast.warning(`Approval Premium Description needed!`) + if (page === 'kyc' && formData.destinationGroup === "Agent" && !formData.approval_description_premium) return toast.warning(`Approval Agent Description needed!`) + // if (page === 'kyc' && !formData.description) return toast.warning(`Description for approval needed!`) + handleSubmit(formData); + } + + function btnPrevDef(e:any) { + e.preventDefault(); + viewOnly ? setViewOnly(false) : setViewOnly(true) + } + + const onReject = () => { + if (formData.destinationGroup === "Premium" && !formData.approval_description_premium) return toast.warning(`Approval Premium Description needed!`) + if (formData.destinationGroup === "Agent" && !formData.approval_description_premium) return toast.warning(`Approval Agent Description needed!`) + handleReject(formData); + handleClose(); + } + + return ( + handleAddDialog(open)}> + + + Member - View/Edit + + + +
+ {alert.show && ( + +

{alert.message}

+
+ )} + + {/*
*/} + {/* onSubmit={(e) => buttonOnSubmit(e, formData)} */} +
+ + {formData.id ? generateInput(formData, handleChange, 'Group', 'group_name', 'text', true, true): ''} + {formData.id ? generateInput(formData, handleChange, 'Username', 'username', 'text', true, true): ''} + {formData.id ? generateInput(formData, handleChange, 'Full Name', 'fullname', 'text', true, viewOnly): ''} + {formData.id ? generateImage(formData, handleChange, 'Photo', 'photouser'): ''} + {formData.id ? generateList(formData, handleChange, genders, 'gender', 'Gender', null, true): ''} + {formData.id ? generateInput(formData, handleChange, 'Date of Birth', 'date_birth', 'date', true, viewOnly): ''} + {formData.id ? generateInput(formData, handleChange, 'Phone Number', 'msisdn', 'text', true, viewOnly): ''} + {formData.id ? generateInput(formData, handleChange, 'Email', 'email', 'text', true, viewOnly): ''} + {/* {generateList(formData, handleChange, status, 'status', 'Status', null, true)} */} + {formData.id ? generateList(formData, handleChange, profession, 'profession', 'Profession', null, false): ''} + {formData.id ? generateList(formData, handleChange, municipios, 'municipio', 'Municipio', null, false): ''} + {formData.id ? generateList(formData, handleChange, postoAdm, 'posto_adms', 'Posto', 'posto_adms_id', false): ''} + {formData.id ? generateList(formData, handleChange, sucos, 'suco', 'Suco', 'sucos_id', false): ''} + {formData.id ? generateList(formData, handleChange, aldeias, 'aldeia', 'Aldeia', null, false): ''} + {formData.id ? generateInput(formData, handleChange, 'Nationality', 'nationality', 'text', true, viewOnly): ''} +
+ +
    + {nationality.map((item: any, index) => ( +
  • handleChange({target:{name: 'nationality', value: item.name }})} + className="bg-gray-100 px-4 py-2 rounded hover:bg-gray-200 cursor-default transition"> + {item.name} +
  • + ))} + {formData.nationality && nationality.length === 0 && ( +
  • No results found.
  • + )} +
+
+ {formData.id ? generateInput(formData, handleChange, 'Address', 'address', 'text', true, viewOnly): ''} + {formData.id ? generateInput(formData, handleChange, 'Mother Fullname', 'mother_fullname', 'text', true, viewOnly): ''} + {formData.id ? generateInput(formData, handleChange, 'Identity Number', 'identity_number', 'text', false, viewOnly): ''} + + {formData.id ? generateInput(formData, handleChange, 'License Number', 'license_number', 'text', false, viewOnly): ''} + {formData.id ? generateInput(formData, handleChange, 'Merchant Name', 'agent_name', 'text', false, viewOnly): ''} + {formData.id ? generateInput(formData, handleChange, 'Merchant Address', 'merchantaddress', 'text', false, viewOnly): ''} + {formData.id ? generateInput(formData, handleChange, 'Longitude Merchant', 'longitudemerchant', 'text', false, viewOnly): ''} + {formData.id ? generateInput(formData, handleChange, 'Latitude Merchant', 'latitudemerchant', 'text', false, viewOnly): ''} + {formData.id ? generateImage(formData, handleChange, 'File Selfie', 'file_selfie'): ''} + {formData.id ? generateImage(formData, handleChange, 'File Document', 'file_document_id'): ''} + {formData.id ? generateImage(formData, handleChange, 'File Document & Selfie', 'file_document_id_selfie'): ''} + {formData.id ? generateImage(formData, handleChange, 'File Commercial License', 'file_commercial_license'): ''} + + {formData.id ? generateList(formData, handleChange, banks, 'bank_name', 'Bank Name', null, false): ''} + {formData.id ? generateInput(formData, handleChange, 'Bank Account', 'bank_account', 'text', false, viewOnly): ''} + {formData.id ? generateInput(formData, handleChange, 'iBank Number', 'ibank_number', 'text', false, viewOnly): ''} + + {(formData.id&&(!page||formData.destinationGroup === "Premium")) ? generateInput(formData, handleChange, 'Approval Premium Description', 'approval_description_premium', 'text', false, viewOnly): ''} + {(formData.id&&(!page||formData.destinationGroup === "Agent")) ? generateInput(formData, handleChange, 'Approval Agent Description', 'approval_description_agent', 'text', false, viewOnly): ''} + + {(formData.id && page!=='kyc') ? AdmAccess(page, formData, handleClose,fetchCustomers, viewOnly, setViewOnly): ""} + {(formData.id && page!=='kyc') ? CustomerWallet(formData.id) : ""} + +
+ + + {/* */} + { + formData.isneedapproval == 1 && page === 'kyc' ? ( + + ) : ('') + } + +
+
+ {/*
*/} +
+
+
+
+ ); +}; + +export default DetailMember; +{/* (agent) */} + +function generateInput(formData:any, handleChange:any, label:string, name:string, type: string, required: boolean, disabled: boolean) { + function generateDate(isoString: string) { + const date = new Date(isoString); + return date.toISOString().slice(0, 10); // "2000-01-18" + } + return ( + <> +
+
+ + +
+
+ + ) +} + +// ON DEV (DI SELECT MASI HILANG) +function generateList(formData:any, handleChange: any, list:any, name:string, label: string, difId: any, required: boolean) { + return ( + <> +
+
+ + +
+
+ + ) +} + +// ON DEV (UPDATENYA) +function generateImage(formData:any, handleChange:any, label:string, name:string) { + const imagePreview = (file:any) => { + if (file && file.type && file.type.startsWith('image/')) { + const previewURL = URL.createObjectURL(file); + return previewURL + } + return file + }; + + return ( + <> +
+ +
+ { formData[name] ? ( +
+ {name} + {/* {name} */} +
+ ) : (No Data)} + +
+
+ + ) +} diff --git a/src/pages/transaction/approval-transaction/blocks/ApprovalDialog.tsx b/src/pages/transaction/approval-transaction/blocks/ApprovalDialog.tsx index 6108b50..779e189 100644 --- a/src/pages/transaction/approval-transaction/blocks/ApprovalDialog.tsx +++ b/src/pages/transaction/approval-transaction/blocks/ApprovalDialog.tsx @@ -1,14 +1,13 @@ import { useTransactionContext } from '../hooks/useApprovalTransactionContext'; import { useCallApi } from '@/hooks'; import { apiConfig } from '@/config/api.config'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { Dialog, DialogBody, DialogContent, - DialogDescription, DialogHeader, - DialogTitle + DialogTitle, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { @@ -16,9 +15,9 @@ import { SelectContent, SelectItem, SelectTrigger, - SelectValue + SelectValue, } from '@/components/ui/select'; -import { Alert, KeenIcon, useDataGrid } from '@/components'; +import { Alert, useDataGrid } from '@/components'; import { doSaveLogActivity } from '@/actions/GlobalActions'; import { toast } from 'sonner'; import { Input } from '@/components/ui/input'; @@ -27,11 +26,11 @@ const API_URL = apiConfig.transaction; const ApprovalDialog = () => { const { GetData, PostData } = useCallApi(); - // const { reload } = useDataGrid(); + const { showApprovalDialog, setShowApprovalDialog, - selectedTransactionIdForApproval + selectedTransactionIdForApproval, } = useTransactionContext(); const [transactionDetails, setTransactionDetails] = useState(null); @@ -39,12 +38,12 @@ const ApprovalDialog = () => { const [formField, setFormField] = useState({ transaction_code: '', status: '', - notes: '' + notes: '', }); const [alert, setAlert] = useState({ show: false, - message: '' + message: '', }); const doApproval = useCallback( @@ -60,39 +59,51 @@ const ApprovalDialog = () => { toast.error('Please select a status.'); return; } - const response = await PostData(`${API_URL}/transaction/set-approval`, { - transaction_code: transactionDetails.id, + id_transaction: transactionDetails.id, status: formField.status, - notes: formField.notes + notes: formField.notes, }); if (response?.status) { - setAlert((prev) => ({ ...prev, show: false, message: '' })); + setAlert({ show: false, message: '' }); toast.success('Success Update Position'); const createActivity = { module: 'Approval Transaction', description: `Change status approve for transaction => ${transactionDetails.code}`, - action: 'U' + action: 'U', }; doSaveLogActivity(createActivity); - setShowApprovalDialog(false); // optionally close dialog + setShowApprovalDialog(false); } else { - setAlert((prev) => ({ ...prev, show: true, message: response?.message })); + setAlert({ show: true, message: response?.message }); } }, [formField, transactionDetails] ); + useEffect(() => { + if (showApprovalDialog) { + // Reset form fields when dialog opens + setFormField({ + transaction_code: '', + status: '', + notes: '', + }); + setTransactionDetails(null); // Optional reset + } + }, [showApprovalDialog]); + useEffect(() => { const fetchTransactionDetails = async () => { if (selectedTransactionIdForApproval) { try { - const response = await GetData(`${API_URL}/transaction/history/detail/${selectedTransactionIdForApproval}`, { - id: selectedTransactionIdForApproval - }); - // console.log(response?.data.code); - // console.log(selectedTransactionIdForApproval); + const response = await GetData( + `${API_URL}/transaction/history/detail/${selectedTransactionIdForApproval}`, + { + id: selectedTransactionIdForApproval, + } + ); setTransactionDetails(response?.data); } catch (error) { console.error('Error fetching transaction', error); @@ -105,6 +116,15 @@ const ApprovalDialog = () => { } }, [showApprovalDialog, selectedTransactionIdForApproval, GetData]); + // Set formField.transaction_code once details are fetched + useEffect(() => { + if (transactionDetails) { + setFormField((prev) => ({ + ...prev, + transaction_code: transactionDetails.id ?? '', + })); + } + }, [transactionDetails]); return ( @@ -113,7 +133,7 @@ const ApprovalDialog = () => { Approval Transaction -
+
@@ -122,7 +142,9 @@ const ApprovalDialog = () => {
+ onChange={(e) => + setFormField((prev) => ({ + ...prev, + notes: e.target.value, + })) + } + />
)} @@ -164,4 +193,4 @@ const ApprovalDialog = () => { ); }; -export default ApprovalDialog; +export default ApprovalDialog; \ No newline at end of file diff --git a/src/pages/transaction/approval-transaction/blocks/DetailApprovalTransaction.tsx b/src/pages/transaction/approval-transaction/blocks/DetailApprovalTransaction.tsx index e0d0ecc..e032a94 100644 --- a/src/pages/transaction/approval-transaction/blocks/DetailApprovalTransaction.tsx +++ b/src/pages/transaction/approval-transaction/blocks/DetailApprovalTransaction.tsx @@ -66,6 +66,24 @@ const DetailApprovalTransaction = () => { > Origin Customer + + +

Status

-

+

{(() => { let status; + let badgeClass; if (transactionDetails?.status === 'C') { status = 'COMPLETE'; + badgeClass = 'bg-green-100 text-green-800'; } else if (transactionDetails?.status === 'F') { status = 'FAILED'; + badgeClass = 'bg-red-100 text-red-800'; } else if (transactionDetails?.status === 'O') { status = 'ON PROCESS'; + badgeClass = 'bg-blue-100 text-blue-800'; } else { status = 'PENDING'; + badgeClass = 'bg-gray-100 text-gray-800'; } - return status; + + return ( + + {status} + + ); })()} -

+
+

Transaction Type

@@ -172,14 +201,7 @@ const DetailApprovalTransaction = () => {
-
-

Name

-

{transactionDetails?.origin_wallet.name}

-
-
-

Description

-

{transactionDetails?.origin_wallet.description}

-
+

@@ -208,8 +230,54 @@ const DetailApprovalTransaction = () => {

)} + {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

+

{transactionDetails?.purchase.product.price_point}

+
+
+

Product Type

+

{transactionDetails?.purchase.product.type}

+
+
+

Provider Name

+

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

+
+
+

Provider Type

+

+ {(() => { + let providertype; + if (transactionDetails?.purchase.product.provider.type === 'h2h') { + providertype = 'HOST TO HOST'; + } else if (transactionDetails?.purchase.product.provider.type === 'agent') { + providertype = 'AGENT'; + } + return providertype; + })()} +

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

Transaction Information @@ -291,69 +359,9 @@ const DetailApprovalTransaction = () => {

{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

@@ -378,6 +386,68 @@ const DetailApprovalTransaction = () => {
)} + {activeTab === 'destinationcustomer' && ( +
+

Destination Customer

+
+
+

Name

+

{transactionDetails?.transfer.destination_customer.fullname}

+
+
+

MSISDN

+

{transactionDetails?.transfer.destination_customer.msisdn}

+
+
+

Email

+

{transactionDetails?.transfer.destination_customer.email}

+
+
+

Username

+

{transactionDetails?.transfer.destination_customer.username}

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

Origin Wallet

+
+
+

Name

+

{transactionDetails?.origin_wallet.name}

+
+
+

Description

+

{transactionDetails?.origin_wallet.description}

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

Destination Wallet

+ + {!transactionDetails?.transfer ? ( +
No Data available
+ ) : ( +
+
+

Name

+

{transactionDetails.transfer.destination_wallet.name}

+
+
+

Description

+

{transactionDetails.transfer.destination_wallet.description}

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

Transaction Logs

@@ -443,8 +513,8 @@ const DetailApprovalTransaction = () => { Status - Created At - Updated At + Date + {/* Updated At */} @@ -466,8 +536,19 @@ const DetailApprovalTransaction = () => { return status; })()} - {log.created_at} - {log.updated_at} + + {new Date(log.created_at).toLocaleString('sv-SE', { + timeZone: 'Asia/Jakarta', // kalau kamu mau waktu lokal (optional) + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, + }).replace(' ', ' ')} + + {/* {log.updated_at} */} )) ) : ( diff --git a/src/pages/transaction/approval-transaction/blocks/ListToolbar.tsx b/src/pages/transaction/approval-transaction/blocks/ListToolbar.tsx index 3247955..3a82db3 100644 --- a/src/pages/transaction/approval-transaction/blocks/ListToolbar.tsx +++ b/src/pages/transaction/approval-transaction/blocks/ListToolbar.tsx @@ -18,12 +18,10 @@ const ListToolbar = () => { // useEffect to set the default date values useEffect(() => { const today = new Date(); - const nextWeek = new Date(today); - nextWeek.setDate(today.getDate() + 7); - + const firstDayOfMonth = new Date(today.getFullYear(), today.getMonth(), 1); settrxDate({ - from: formatDate(today), // Set 'from' to today - to: formatDate(nextWeek), // Set 'to' to 7 days later + from: formatDate(firstDayOfMonth), + to: formatDate(today), }); }, []); diff --git a/src/pages/transaction/approval-transaction/hooks/ApprovalTransactionContext.tsx b/src/pages/transaction/approval-transaction/hooks/ApprovalTransactionContext.tsx index fb4536c..5c74f68 100644 --- a/src/pages/transaction/approval-transaction/hooks/ApprovalTransactionContext.tsx +++ b/src/pages/transaction/approval-transaction/hooks/ApprovalTransactionContext.tsx @@ -163,22 +163,35 @@ const ApprovalTransactionProvider = ({ children }: { children: React.ReactNode } }, }, { - accessorFn: (row) => { - switch (row.status_approve) { - case 'W': return 'WAITING APPROVAL'; - case 'Y': return 'APPROVED'; - case 'N': return 'REJECTED'; - default: return 'PENDING'; + accessorKey: 'status_approve', + header: 'Status', + cell: ({ row }) => { + const statusCode = row.original.status_approve; + let label = ''; + let badgeClass = ''; + + switch (statusCode) { + case 'Y': + label = 'APPROVED'; + badgeClass = 'bg-green-100 text-green-800'; + break; + case 'N': + label = 'REJECTED'; + badgeClass = 'bg-red-100 text-red-800'; + break; + case 'W': + label = 'WAITING APPROVAL'; + badgeClass = 'bg-gray-100 text-gray-800'; + break; } + + return ( + + {label} + + ); }, - id: 'status_approve', - header: ({ column }) => , - enableSorting: false, - enableHiding: false, - meta: { - headerClassName: 'w-[250px]', - }, - }, + }, { id: 'actions', header: ({ column }) => , diff --git a/src/pages/transaction/history-transaction/blocks/DetailTransaction.tsx b/src/pages/transaction/history-transaction/blocks/DetailTransaction.tsx index 6b44bdc..55db0a0 100644 --- a/src/pages/transaction/history-transaction/blocks/DetailTransaction.tsx +++ b/src/pages/transaction/history-transaction/blocks/DetailTransaction.tsx @@ -30,7 +30,6 @@ const DetailTransaction = () => { 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); @@ -66,6 +65,24 @@ const DetailTransaction = () => { > Origin Customer + + +

Status

-

+

{(() => { let status; + let badgeClass; if (transactionDetails?.status === 'C') { status = 'COMPLETE'; + badgeClass = 'bg-green-100 text-green-800'; } else if (transactionDetails?.status === 'F') { status = 'FAILED'; + badgeClass = 'bg-red-100 text-red-800'; } else if (transactionDetails?.status === 'O') { status = 'ON PROCESS'; + badgeClass = 'bg-blue-100 text-blue-800'; } else { status = 'PENDING'; + badgeClass = 'bg-gray-100 text-gray-800'; } - return status; + + return ( + + {status} + + ); })()} -

+
+

Transaction Type

@@ -172,14 +200,7 @@ const DetailTransaction = () => {
-
-

Name

-

{transactionDetails?.origin_wallet.name}

-
-
-

Description

-

{transactionDetails?.origin_wallet.description}

-
+

@@ -246,7 +267,7 @@ const DetailTransaction = () => { providertype = 'HOST TO HOST'; } else if (transactionDetails?.purchase.product.provider.type === 'agent') { providertype = 'AGENT'; - } + } return providertype; })()}

@@ -337,69 +358,9 @@ const DetailTransaction = () => {

{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

@@ -424,6 +385,68 @@ const DetailTransaction = () => {
)} + {activeTab === 'destinationcustomer' && ( +
+

Destination Customer

+
+
+

Name

+

{transactionDetails?.transfer.destination_customer.fullname}

+
+
+

MSISDN

+

{transactionDetails?.transfer.destination_customer.msisdn}

+
+
+

Email

+

{transactionDetails?.transfer.destination_customer.email}

+
+
+

Username

+

{transactionDetails?.transfer.destination_customer.username}

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

Origin Wallet

+
+
+

Name

+

{transactionDetails?.origin_wallet.name}

+
+
+

Description

+

{transactionDetails?.origin_wallet.description}

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

Destination Wallet

+ + {!transactionDetails?.transfer ? ( +
No Data available
+ ) : ( +
+
+

Name

+

{transactionDetails.transfer.destination_wallet.name}

+
+
+

Description

+

{transactionDetails.transfer.destination_wallet.description}

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

Transaction Logs

@@ -489,8 +512,8 @@ const DetailTransaction = () => { Status - Created At - Updated At + Date + {/* Updated At */} @@ -512,8 +535,19 @@ const DetailTransaction = () => { return status; })()} - {log.created_at} - {log.updated_at} + + {new Date(log.created_at).toLocaleString('sv-SE', { + timeZone: 'Asia/Jakarta', // kalau kamu mau waktu lokal (optional) + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, + }).replace(' ', ' ')} + + {/* {log.updated_at} */} )) ) : ( diff --git a/src/pages/transaction/history-transaction/blocks/ListToolbar.tsx b/src/pages/transaction/history-transaction/blocks/ListToolbar.tsx index afc8ac5..e0a15c3 100644 --- a/src/pages/transaction/history-transaction/blocks/ListToolbar.tsx +++ b/src/pages/transaction/history-transaction/blocks/ListToolbar.tsx @@ -18,12 +18,10 @@ const ListToolbar = () => { // useEffect to set the default date values useEffect(() => { const today = new Date(); - const nextWeek = new Date(today); - nextWeek.setDate(today.getDate() + 7); - + const firstDayOfMonth = new Date(today.getFullYear(), today.getMonth(), 1); settrxDate({ - from: formatDate(today), // Set 'from' to today - to: formatDate(nextWeek), // Set 'to' to 7 days later + from: formatDate(firstDayOfMonth), + to: formatDate(today), }); }, []); diff --git a/src/pages/transaction/history-transaction/hooks/TransactionContext.tsx b/src/pages/transaction/history-transaction/hooks/TransactionContext.tsx index 273dd4f..6a39771 100644 --- a/src/pages/transaction/history-transaction/hooks/TransactionContext.tsx +++ b/src/pages/transaction/history-transaction/hooks/TransactionContext.tsx @@ -84,7 +84,7 @@ const TransactionProvider = ({ children }: { children: React.ReactNode }) => { }); return formattedDateTime; // Format DD-MM-YYYY HH:MM:SS (menggunakan waktu yang sudah ada) } - }, + }, { accessorKey: 'origin_customer.fullname', header: ({ column }) => , @@ -125,19 +125,6 @@ const TransactionProvider = ({ children }: { children: React.ReactNode }) => { }, }, { - 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, @@ -145,7 +132,37 @@ const TransactionProvider = ({ children }: { children: React.ReactNode }) => { meta: { headerClassName: 'w-[250px]', }, - }, + cell: ({ row }) => { + const statusCode = row.original.status; + let label = ''; + let badgeClass = ''; + + switch (statusCode) { + case 'C': + label = 'COMPLETE'; + badgeClass = 'bg-green-100 text-green-800'; + break; + case 'F': + label = 'FAILED'; + badgeClass = 'bg-red-100 text-red-800'; + break; + case 'O': + label = 'ON PROCESS'; + badgeClass = 'bg-blue-100 text-blue-800'; + break; + default: + label = 'PENDING'; + badgeClass = 'bg-gray-100 text-gray-800'; + break; + } + + return ( + + {label} + + ); + }, + }, { accessorKey: 'description', header: ({ column }) => , @@ -201,11 +218,12 @@ const TransactionProvider = ({ children }: { children: React.ReactNode }) => { 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]; + // Tanggal 1 di bulan sekarang + const firstDayOfMonth = new Date(today.getFullYear(), today.getMonth(), 1); + + startdate = firstDayOfMonth.toISOString().split('T')[0]; + enddate = today.toISOString().split('T')[0]; } else if (filter != undefined || filter.length != 0) { startdate = filter[0].value.from; enddate = filter[0].value.to; diff --git a/src/pages/transfer/transfertype/TransferType.tsx b/src/pages/transfer/transfertype/TransferType.tsx index 49d7abe..ab79eca 100644 --- a/src/pages/transfer/transfertype/TransferType.tsx +++ b/src/pages/transfer/transfertype/TransferType.tsx @@ -5,7 +5,7 @@ import { } from './hooks/ManageTransferTypeContext'; import AddDialog from './blocks/AddDialog'; import { Breadcrumbs, Link } from '@mui/material'; -import { DeleteDialog } from './blocks/DeleteDialog'; +import DeleteDialog from './blocks/DeleteDialog'; import { EditDialog } from './blocks/EditDialog'; import { Helmet } from 'react-helmet'; @@ -37,7 +37,7 @@ const TransferType = () => {
- + {/* */} diff --git a/src/pages/transfer/transfertype/blocks/AddDialog.tsx b/src/pages/transfer/transfertype/blocks/AddDialog.tsx index 19760af..bff4222 100644 --- a/src/pages/transfer/transfertype/blocks/AddDialog.tsx +++ b/src/pages/transfer/transfertype/blocks/AddDialog.tsx @@ -218,7 +218,7 @@ const AddDialog = () => { order_direction: 'ASC', }; const response = await GetData(`${API_URL_MASTERDATA}/wallet/list`, params); - // console.log(response) + console.log(response) if (response?.status && response?.data) { setWallets(response.data.list); } else { @@ -230,7 +230,7 @@ const AddDialog = () => { if (!showAddDialog) return; fetchWallets(); }, [showAddDialog]); - // console.log(formField) + console.log(formField) return ( handleAddDialog(open)}> diff --git a/src/pages/transfer/transfertype/blocks/DeleteDialog.tsx b/src/pages/transfer/transfertype/blocks/DeleteDialog.tsx index ce6a1b8..19e4d78 100644 --- a/src/pages/transfer/transfertype/blocks/DeleteDialog.tsx +++ b/src/pages/transfer/transfertype/blocks/DeleteDialog.tsx @@ -19,15 +19,12 @@ const DeleteDialog = () => { message: '' }); - - - const doDeleteTransferType = useCallback(async (id:string|null) => { + const doDeleteTransferType = useCallback(async () => { if (!selectedTransferType) { toast.error('No Transfer Type selected'); return; } - // Kirim enforce=false untuk memastikan soft delete const response = await DeleteData(`${API_URL}/transactiontype/delete/${selectedTransferType}/false`, { id: selectedTransferType }); @@ -36,18 +33,12 @@ const DeleteDialog = () => { setAlert({ show: false, message: '' }); handleDeleteDialog(false, null); reload(); - setTimeout(() => toast.success('Success Delete Transaction Type'), 0); + // setTimeout(() => toast.success('Success Delete Transaction Type'), 0); } else { setAlert({ show: true, message: response?.message }); // setTimeout(() => toast.error('Failed Delete Product'), 0); } - }, [selectedTransferType, DeleteData, handleDeleteDialog, reload]); - - const handleDelete = ((id:string|null) => { - console.log(id); - doDeleteTransferType(id); - }) - + }, [selectedTransferType]); return ( handleDeleteDialog(open, null)}> @@ -68,7 +59,7 @@ const DeleteDialog = () => { - @@ -78,4 +69,3 @@ const DeleteDialog = () => { }; export default DeleteDialog; -export { DeleteDialog }; diff --git a/src/pages/transfer/transfertype/blocks/EditDialog.tsx b/src/pages/transfer/transfertype/blocks/EditDialog.tsx index 8f52633..db7e391 100644 --- a/src/pages/transfer/transfertype/blocks/EditDialog.tsx +++ b/src/pages/transfer/transfertype/blocks/EditDialog.tsx @@ -25,23 +25,16 @@ import { useCallApi } from '@/hooks'; import { getAuth } from '@/auth'; import { ManageTransferFeeContextProvider } from '../../transferfee/hooks/ManageTransferFeeContext'; import AddFeeDialog from '../../transferfee/blocks/AddDialog'; -import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList -} from '@/components/ui/command'; +import { Checkbox } from '@/components/ui/checkbox'; +import { doSaveLogActivity } from '@/actions/GlobalActions'; const API_URL = apiConfig.service_transaction; const API_URL_MASTERDATA = apiConfig.service_master_data; const API_URL_CUSTOMER = apiConfig.service_customer; interface WalletProps { - Wallet_id: string; - Wallet_name: string; + id: string; + name: string; } interface CustomerProps { @@ -50,19 +43,11 @@ interface CustomerProps { msisdn: string; } -interface TranssactionTypeProps { +interface GroupProps { + id: string; name: string; description: string; - minimum_amount: number; - maximum_amount: number; - max_transaction_per_day: number; - status_approval: string; status: string; - type: string; - wallet_origin: WalletProps; - wallet_destination: WalletProps; - wallet_fee_destination: WalletProps; - customer_fee_destination: CustomerProps; } const EditDialog = () => { @@ -71,29 +56,43 @@ const EditDialog = () => { useManageTransferTypeContext(); const { reload } = useDataGrid(); const [wallets, setWallets] = useState([]); + const [groups, setGroups] = useState([]); const { GetData, PutData } = useCallApi(); const [isSubmitting, setIsSubmitting] = useState(false); const parsedUser = getAuth()?.user; const [customers, setCustomers] = useState([]); - const [open, setOpen] = useState(false); + const [selectedGroups, setSelectedGroups] = useState([]); const [alert, setAlert] = useState({ show: false, message: '' }); - const initialState = { + const initialState: { + name: string; + description: string; + wallet_origin: string; + wallet_destination: string; + minimum_amount: number; + maximum_amount: number; + max_transaction_per_day: number; + status: string; + type: string; + status_approval: string; + updated_by: string; + updated_at: string; + permission: string[]; + } = { name: '', description: '', wallet_origin: '', wallet_destination: '', - wallet_fee_destination: '', - customer_fee_destination: '', minimum_amount: 0, maximum_amount: 0, max_transaction_per_day: 0, status_approval: '', type: '', status: '', + permission: [], updated_by: '', updated_at: '' }; @@ -102,27 +101,47 @@ const EditDialog = () => { const resetForm = () => { setFormField(initialState); + setSelectedGroups([]); + setAlert({ show: false, message: '' }); + }; + + const handleGroupChange = (groupId: string) => { + setFormField((prevState) => { + const isSelected = prevState.permission.includes(groupId); + + if (isSelected) { + // Remove the permission if already selected + return { + ...prevState, + permission: prevState.permission.filter((id) => id !== groupId) + }; + } else { + // Add the permission if not selected + return { + ...prevState, + permission: [...prevState.permission, groupId] + }; + } + }); }; - // Validation function to make certain fields required const validateForm = () => { const requiredFields = [ 'name', 'description', 'wallet_origin', 'wallet_destination', - 'wallet_fee_destination', - 'customer_fee_destination', 'status', 'status_approval', 'type' ]; const missingFields = requiredFields.filter( - (field) => - formField[field as keyof typeof formField] === '' || - formField[field as keyof typeof formField] === null || - formField[field as keyof typeof formField] === undefined + (field) => { + return formField[field as keyof typeof formField] === '' || + formField[field as keyof typeof formField] === null || + formField[field as keyof typeof formField] === undefined; + } ); if (missingFields.length > 0) { @@ -133,6 +152,15 @@ const EditDialog = () => { return false; } + // Validate that at least one permission is selected + if (formField.permission.length === 0) { + setAlert({ + show: true, + message: 'Please select at least one group permission' + }); + return false; + } + setAlert({ show: false, message: '' }); return true; }; @@ -148,7 +176,12 @@ const EditDialog = () => { updated_at: formattedTime })); } - }, [showEditDialog]); + }, [showEditDialog, parsedUser]); + + const selectedPermissionNames = groups + .filter((g) => formField.permission.includes(g.id)) + .map((g) => g.name) + .join(', '); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); @@ -165,6 +198,14 @@ const EditDialog = () => { handleEditDialog(false, null); toast.success('Success Update Transfer Type'); reload(); + + const createActivity = { + module: 'Manage Transfer Type', + description: `Edit Transfer Type => ${selectedTransferType}`, + action: 'U' + }; + + doSaveLogActivity(createActivity); } }) .finally(() => { @@ -197,11 +238,13 @@ const EditDialog = () => { } }, [formField, selectedTransferType, PutData]); + // Fetch customers useEffect(() => { if (!showEditDialog) return; - const getCustomerList = async (sorting: any) => { + + const getCustomerList = async () => { try { - sorting = sorting.length === 0 ? [{ id: 'name', desc: false }] : sorting; + const sorting = [{ id: 'id', desc: false }]; const response = await GetData(`${API_URL_CUSTOMER}/customer/list`, { limit: 100, page: 1, @@ -209,74 +252,113 @@ const EditDialog = () => { order_field: sorting[0].id, order_direction: sorting[0].desc ? 'DESC' : 'ASC' }); - setCustomers(response?.data.list); + + if (response?.status && response?.data) { + setCustomers(response.data.list); + } } catch (error) { console.error('Error fetching customer', error); } }; - getCustomerList([{ id: 'id', desc: false }]); + getCustomerList(); }, [showEditDialog, GetData]); - - const fetchWallets = useCallback(async () => { - const params = { - limit: 100, - page: 1, - with_deleted: false, - order_field: 'name', - order_direction: 'ASC', - filter: JSON.stringify({ - status: 'Y' - }) - }; - const response = await GetData(`${API_URL_MASTERDATA}/wallet/list`, params); - if (response?.status && response?.data) { - setWallets(response.data.list); - } else { - setWallets([]); - } - }, [GetData]); + // Fetch groups useEffect(() => { if (!showEditDialog) return; - fetchWallets(); - }, [showEditDialog, fetchWallets]); - - const fetchTransactionType = useCallback(async (id: string) => { - try { - const response = await GetData(`${API_URL}/transactiontype/getdata/${id}`, { id }); - if (response?.status) { - setFormField((prev) => ({ - ...prev, - name: response.data.name, - description: response.data.description, - wallet_origin: response.data.wallet_origin.id, - wallet_destination: response.data.wallet_destination.id, - wallet_fee_destination: response.data.wallet_fee_destination.id, - customer_fee_destination: response.data.customer_fee_destination.id, - minimum_amount: response.data.minimum_amount, - maximum_amount: response.data.maximum_amount, - max_transaction_per_day: response.data.max_transaction_per_day, - status_approval: response.data.status_approval, - type: response.data.type || '', - status: response.data.status - })); + + const getGroupList = async () => { + try { + const response = await GetData(`${API_URL_MASTERDATA}/groups/list`, { + limit: 100, + page: 1, + with_deleted: false, + order_field: 'name', + order_direction: 'ASC' + }); + + if (response?.status && response?.data) { + setGroups(response.data.list); + } + } catch (error) { + console.error('Error fetching groups', error); } - // console.log(response); - } catch (error) { - console.error('Error fetching transaction type', error); - setAlert({ - show: true, - message: 'Failed to load transaction type data' - }); - } - }, [GetData]); + }; + + getGroupList(); + }, [showEditDialog, GetData]); + + // Fetch wallets + useEffect(() => { + if (!showEditDialog) return; + + const getWalletList = async () => { + try { + const response = await GetData(`${API_URL_MASTERDATA}/wallet/list`, { + limit: 100, + page: 1, + with_deleted: false, + order_field: 'wallets.name', + order_direction: 'ASC', + }); + + if (response?.status && response?.data) { + setWallets(response.data.list); + } + } catch (error) { + console.error('Error fetching wallets', error); + } + }; + + getWalletList(); + }, [showEditDialog, GetData]); + + // Fetch transaction type data + useEffect(() => { + if (!showEditDialog || !selectedTransferType) return; + + const fetchTransactionType = async () => { + try { + const response = await GetData(`${API_URL}/transactiontype/getdata/${selectedTransferType}`, {}); + + if (response?.status) { + setFormField((prev) => ({ + ...prev, + name: response.data.name, + description: response.data.description, + wallet_origin: response.data.wallet_origin?.id || '', + wallet_destination: response.data.wallet_destination?.id || '', + minimum_amount: response.data.minimum_amount, + maximum_amount: response.data.maximum_amount, + max_transaction_per_day: response.data.max_transaction_per_day, + status_approval: response.data.status_approval, + type: response.data.type || '', + status: response.data.status, + permission: response.data.permission || [] + })); + } + } catch (error) { + console.error('Error fetching transaction type', error); + setAlert({ + show: true, + message: 'Failed to load transaction type data' + }); + } + }; + + const timer = setTimeout(() => { + fetchTransactionType(); + }, 150); + + return () => clearTimeout(timer); + }, [showEditDialog, selectedTransferType, GetData]); useEffect(() => { - if (selectedTransferType) { - fetchTransactionType(selectedTransferType); + if (showEditDialog === false) { + resetForm(); } - }, [selectedTransferType, fetchTransactionType]); + }, [showEditDialog]); return ( handleEditDialog(open, null)}> @@ -305,9 +387,11 @@ const EditDialog = () => {
{alert.show && ( - -

{alert.message}

-
+
+ +

{alert.message}

+
+
)}
@@ -439,8 +523,8 @@ const EditDialog = () => { {wallets.map((wallet) => ( - - {wallet.Wallet_name} + + {wallet.name} ))} @@ -467,8 +551,8 @@ const EditDialog = () => { {wallets.map((wallet) => ( - - {wallet.Wallet_name} + + {wallet.name} ))} @@ -476,86 +560,6 @@ const EditDialog = () => {
-
-
- -
- -
-
-
-
-
- -
- - - - - - - - { - e.currentTarget.scrollTop += e.deltaY; - }} - > - No Customer found. - - {customers.map((customer) => ( - { - setFormField({ - ...formField, - customer_fee_destination: customer.id - }); - setOpen(false); - }} - > - {customer.username} - {customer.msisdn} - - ))} - - - - - -
-
-
@@ -637,7 +641,47 @@ const EditDialog = () => {
- + + {/* Group Permission Section - Read Only Display */} +
+
+ +
+ + + {/* Groups Selection Area */} +
+
+ {groups.map((group) => ( +
+ handleGroupChange(group.id)} + /> + +
+ ))} +
+
+
+
+
+
- {/* Transaction Fee Section */} diff --git a/src/pages/transfer/transfertype/hooks/ManageTransferTypeContext.tsx b/src/pages/transfer/transfertype/hooks/ManageTransferTypeContext.tsx index f5fcd14..2fde89a 100644 --- a/src/pages/transfer/transfertype/hooks/ManageTransferTypeContext.tsx +++ b/src/pages/transfer/transfertype/hooks/ManageTransferTypeContext.tsx @@ -76,6 +76,16 @@ const ManageTransferTypeContextProvider = ({ children }: { children: React.React const columns = useMemo[]>( () => [ + { + accessorFn: (row) => row.id, + id: 'id', + header: ({ column }) => ( + + ), + enableSorting: false, + enableHiding: false, + meta: { headerClassName: 'w-[250px]' } + }, { accessorFn: (row) => row.name, id: 'name', @@ -213,7 +223,7 @@ const ManageTransferTypeContextProvider = ({ children }: { children: React.React order_direction: orderDirection, filter: JSON.stringify(filter) }); - + console.log(response?.data.list); return { data: response?.data.list, totalCount: response?.data.total_count }; }; diff --git a/src/pages/wallet/wallet-history/hooks/ManageWalletHistoryContext.tsx b/src/pages/wallet/wallet-history/hooks/ManageWalletHistoryContext.tsx index 95b7aca..e2b2edd 100644 --- a/src/pages/wallet/wallet-history/hooks/ManageWalletHistoryContext.tsx +++ b/src/pages/wallet/wallet-history/hooks/ManageWalletHistoryContext.tsx @@ -82,7 +82,7 @@ const ManageWalletContextProvider = ({ children }: { children: React.ReactNode } }, { accessorKey: 'amount' , - header: ({ column }) => , + header: ({ column }) => , enableSorting: false, enableHiding: false, meta: { @@ -108,8 +108,10 @@ const ManageWalletContextProvider = ({ children }: { children: React.ReactNode } } }, { - accessorKey: 'CreatedAt' , - header: ({ column }) => , + accessorKey: 'CreatedAt', + header: ({ column }) => ( + + ), cell: ({ row }) => new Date(row.original.CreatedAt).toLocaleString('id-ID', { day: '2-digit', @@ -118,7 +120,7 @@ const ManageWalletContextProvider = ({ children }: { children: React.ReactNode } hour: '2-digit', minute: '2-digit', }), - enableSorting: false, + enableSorting: false, enableHiding: false, meta: { headerClassName: 'w-[200px]' @@ -174,26 +176,24 @@ const ManageWalletContextProvider = ({ children }: { children: React.ReactNode } headerClassName: 'w-[200px]' } } - ], [] ); - - const getWalletLists = async (page: number, limit: number, sorting: any, filter: any) => { try { - sorting = sorting.length == 0 ? [{ id: 'name', desc: false }] : sorting; + const sortField = 'CreatedAt'; + const sortDirection = 'ASC'; + filter = filter.length == 0 ? {} : { name: filter[0].value?.toLowerCase() }; const response = await GetData(`${API_URL_WALLET}/dashboard/balance/`, { limit, page: page + 1, with_deleted: false, - order_field: sorting[0].id, - order_direction: sorting[0].desc == false ? 'ASC' : 'DESC', + order_field: sortField, + order_direction: sortDirection, // filter: JSON.stringify(filter) }); - // console.log(response?.data); setWallets(response?.data.list); return { data: response?.data.list, totalCount: response?.data.total_count }; } catch (error) { @@ -221,7 +221,7 @@ const ManageWalletContextProvider = ({ children }: { children: React.ReactNode } pagination={{ size: 10 }} toolbar={} layout={{ card: true }} - sorting={[{ id: 'id', desc: false }]} + // sorting={[{ id: 'created_at', desc: true }]} serverSide={true} onFetchData={({ pageIndex, pageSize, sorting, columnFilters }) => getWalletLists(pageIndex, pageSize, sorting, columnFilters) @@ -234,4 +234,4 @@ const ManageWalletContextProvider = ({ children }: { children: React.ReactNode } }; export { ManageWalletContext, ManageWalletContextProvider }; -export type { WalletProps }; +export type { WalletProps }; \ No newline at end of file diff --git a/src/routing/AppRoutingSetup.tsx b/src/routing/AppRoutingSetup.tsx index 98f78b3..b7ce41f 100644 --- a/src/routing/AppRoutingSetup.tsx +++ b/src/routing/AppRoutingSetup.tsx @@ -40,6 +40,10 @@ import WalletHistory from '@/pages/wallet/wallet-history/WalletHistory'; import WalletMaster from '@/pages/master/wallet/WalletMaster'; import CurrencyMaster from '@/pages/master/currency/CurrencyMaster'; +// DISBURSEMENT +import HistoryTransactionDisbursement from '@/pages/disbursement/history-transaction/HistoryTransaction' +// DISBURSEMENT + const AppRoutingSetup = (): ReactElement => { return ( @@ -100,6 +104,11 @@ const AppRoutingSetup = (): ReactElement => { path="/settings/user-management/manage-position" element={} /> + + {/* DISBURSEMENT */} + } /> + {/* DISBURSEMENT */} + } />