{
+ try {
+ await acceptTransfer({ id, token });
+ onClose();
+ setRefetch(true);
+ } catch (error) {
+ console.error('Error accepting transfer:', error);
+ }
+ };
+
+ const handleDecline = async (id) => {
+ try {
+ await declineTransfer({ id, token });
+ onClose();
+ setRefetch(true);
+ } catch (error) {
+ console.error('Error declining transfer:', error);
+ }
+ };
+
+ const handleCancel = async (id) => {
+ try {
+ await cancelTransfer({ id, token });
+ onClose();
+ setRefetch(true);
+ } catch (error) {
+ console.error('Error cancelling transfer:', error);
+ }
+ };
+
+ const managedWalletsWithDefault = managedWallets.wallets ? managedWallets : { ...managedWallets, wallets: [] };
+
+ const receiverWallet = rowInfo?.receiver_wallet || rowInfo?.destination_wallet;
+ const senderWallet = rowInfo?.sender_wallet || rowInfo?.originating_wallet;
+ const canAcceptDecline =
+ receiverWallet &&
+ (wallet.name === receiverWallet ||
+ managedWalletsWithDefault.wallets.some(w => w.name === receiverWallet));
+
+ const canCancel =
+ senderWallet &&
+ rowInfo?.status === 'pending' &&
+ (wallet.name === senderWallet ||
+ managedWalletsWithDefault.wallets.some(w => w.name === senderWallet));
+
+ return (
+
+
+
+
+
+
+
+
+
+ Transfer Request
+
+
+
+ Transfer ID:
+
+
+
+
+
+
+ Sender Wallet:
+
+
+
+
+
+
+ Receiver Wallet:
+
+
+
+
+
+
+ Initiated By:
+
+
+
+
+
+
+ Created Date:
+
+ {rowInfo?.created_date.split('T')[0] || '--'}
+
+
+
+
+ Token Amount:
+
+ {rowInfo?.token_amount || '--'}
+
+
+
+
+ {rowInfo?.status === 'pending' && canAcceptDecline && (
+
+
handleAccept(rowInfo.id || rowInfo.transfer_id)}
+ >
+ Accept Transfer
+
+
handleDecline(rowInfo.id || rowInfo.transfer_id)}>
+ Decline
+
+
+ )}
+
+ {rowInfo?.status === 'pending' && canCancel && (
+
+ handleCancel(rowInfo.id || rowInfo.transfer_id)}
+ >
+ Cancel Transfer
+
+
+ )}
+
+
+ );
+}
+
+export default TransferSidePanel;
+
diff --git a/src/pages/MyTransfers/TransferSidePanel.styled.js b/src/pages/MyTransfers/TransferSidePanel.styled.js
new file mode 100644
index 0000000..ef9b6a1
--- /dev/null
+++ b/src/pages/MyTransfers/TransferSidePanel.styled.js
@@ -0,0 +1,114 @@
+import MuiDrawer from "@mui/material/Drawer";
+import { styled } from "@mui/system";
+import { Typography, Button } from "@mui/material";
+
+
+const drawerWidth = 320;
+const mobileDrawerWidth = 140;
+
+const openedMixin = (theme) => ({
+ width: mobileDrawerWidth,
+ [theme.breakpoints.up("sm")]: {
+ width: drawerWidth,
+ },
+ transition: theme.transitions.create("width", {
+ easing: theme.transitions.easing.sharp,
+ duration: theme.transitions.duration.enteringScreen,
+ }),
+ overflowX: "hidden",
+});
+
+const closedMixin = (theme) => ({
+ transition: theme.transitions.create("width", {
+ easing: theme.transitions.easing.sharp,
+ duration: theme.transitions.duration.leavingScreen,
+ }),
+ overflowX: "hidden",
+ width: `calc(${theme.spacing(8)} + 1px)`,
+ [theme.breakpoints.up("sm")]: {
+ width: `calc(${theme.spacing(8)} + 1px)`,
+ },
+});
+
+const DrawerHeaderStyled = styled("div")(({ theme }) => ({
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "flex-end",
+ padding: theme.spacing(0, 1),
+ // necessary for content to be below app bar
+ ...theme.mixins.toolbar,
+}));
+
+const DrawerStyled = styled(MuiDrawer, {
+ shouldForwardProp: (prop) => prop !== "open",
+})(({ theme, open }) => ({
+ width: mobileDrawerWidth,
+ [theme.breakpoints.up("sm")]: {
+ width: drawerWidth,
+ },
+ flexShrink: 0,
+ whiteSpace: "nowrap",
+ boxSizing: "border-box",
+ ...(open && {
+ ...openedMixin(theme),
+ "& .MuiDrawer-paper": openedMixin(theme),
+ }),
+ ...(!open && {
+ ...closedMixin(theme),
+ "& .MuiDrawer-paper": closedMixin(theme),
+ }),
+}));
+
+const BoldTypography = styled(Typography)({
+ fontWeight: 'bold',
+ fontSize: '.9rem'
+});
+
+const NormalTypography = styled(Typography)({
+ fontWeight: 400,
+ fontSize: '.9rem',
+ textTransform: 'capitalize'
+});
+
+const DeclineButton = styled(Button)({
+ textTransform: 'none',
+ padding: 0,
+ minWidth: 0,
+ border: 'none',
+ fontWeight: 700,
+ fontSize: '1rem',
+ margin: '0 10px',
+ color: '#FF7A00',
+ '&:hover': {
+ border: 'none',
+ }
+});
+
+const AcceptButton = styled(Button)({
+ textTransform: 'none',
+ minWidth: 0,
+ borderRadius: '20px',
+ fontWeight: 500,
+ fontSize: '1rem',
+ padding: '6px 15px',
+ margin: '0 10px',
+});
+
+const CancelButton = styled(Button)({
+ textTransform: 'none',
+ minWidth: 0,
+ borderRadius: '20px',
+ fontWeight: 500,
+ fontSize: '1rem',
+ padding: '6px 15px',
+ margin: '0 10px',
+ backgroundColor: '#FF7A00',
+ color: '#fff',
+ '&:hover': {
+ backgroundColor: '#e66a00',
+ }
+});
+
+
+export { DrawerHeaderStyled, DrawerStyled, BoldTypography, NormalTypography, DeclineButton, AcceptButton, CancelButton };
+
diff --git a/src/pages/MyTransfers/TransfersTable.js b/src/pages/MyTransfers/TransfersTable.js
index 5ddd087..cc2e693 100644
--- a/src/pages/MyTransfers/TransfersTable.js
+++ b/src/pages/MyTransfers/TransfersTable.js
@@ -11,10 +11,13 @@ import {
Typography,
} from '@mui/material';
import React, { useEffect, useRef, useState } from 'react';
+ import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
+ import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
import { DateRangeFilter, ResetButton, TransferSelectFilter } from './TableFilters';
import { TableCellStyled, TooltipStyled } from './TransfersTable.styled';
import { useTransfersContext } from '../../store/TransfersContext';
import { Loader } from '../../components/UI/components/Loader/Loader';
+ import TransferSidePanel from './TransferSidePanel';
/**@function
* @name TableHeader
@@ -95,10 +98,47 @@ import {
* @param tableColumns
* @param tableRows
* @param getStatusColor
+ * @param selectedRowIndex
+ * @param setSelectedRowIndex
* @return {JSX.Element} - Table body component
*/
- const TransfersTableBody = ({ tableColumns, tableRows, getStatusColor }) => {
- const { isLoading } = useTransfersContext();
+ const TransfersTableBody = ({
+ tableColumns,
+ tableRows,
+ getStatusColor,
+ selectedRowIndex,
+ setSelectedRowIndex,
+ }) => {
+ const { isLoading, managedWallets } = useTransfersContext();
+ const [isSidePanelOpen, setIsSidePanelOpen] = useState(false);
+ const [rowInfo, setRowInfo] = useState(null);
+ const wallet = JSON.parse(localStorage.getItem('wallet') || '{}');
+
+ const handleClosePanel = () => {
+ setIsSidePanelOpen(false);
+ setSelectedRowIndex(null);
+ };
+
+ const handleRowClick = (rowIndex, row) => {
+ setRowInfo(row);
+ setSelectedRowIndex(rowIndex);
+ // Only open side panel for pending transfers
+ if (row.status === 'pending') {
+ setIsSidePanelOpen(true);
+ }
+ };
+
+ // Check if a row requires user action (pending transfer where user can accept/decline)
+ const requiresAction = (row) => {
+ if (row.status !== 'pending') return false;
+ const managedWalletsWithDefault = managedWallets.wallets ? managedWallets : { ...managedWallets, wallets: [] };
+ const receiverWallet = row.receiver_wallet || row.destination_wallet;
+ return (
+ receiverWallet &&
+ (wallet.name === receiverWallet ||
+ managedWalletsWithDefault.wallets.some(w => w.name === receiverWallet))
+ );
+ };
if (isLoading)
return (
@@ -123,37 +163,66 @@ import {
);
return (
-
- {tableRows &&
- tableRows.map((row, rowIndex) => {
- return (
-
- {tableColumns.map((column, colIndex) => {
- const cellKey = `${rowIndex}-${colIndex}-${column.description}`;
- const cellColor =
- column.name === 'status'
- ? getStatusColor(row[column.name])
- : '';
- const cellValue = row[column.name]
- ? column.renderer
- ? column.renderer(row[column.name])
- : row[column.name]
- : '--';
+ <>
+
+ {tableRows &&
+ tableRows.map((row, rowIndex) => {
+ const isSelected = rowIndex === selectedRowIndex;
+ const needsAction = requiresAction(row);
+ return (
+ handleRowClick(rowIndex, row)}
+ sx={{
+ transition: 'all 0.3s ease',
+ cursor: 'pointer',
+ }}
+ style={{
+ backgroundColor:
+ isSelected && needsAction
+ ? 'rgba(135, 195, 46, .4)'
+ : isSelected
+ ? 'rgba(135, 195, 46, .4)'
+ : needsAction
+ ? 'rgba(255, 122, 0, .1)'
+ : null,
+ border: needsAction ? '2px solid rgba(255, 122, 0, .5)' : 'none',
+ }}
+ >
+ {tableColumns.map((column, colIndex) => {
+ const cellKey = `${rowIndex}-${colIndex}-${column.description}`;
+ const cellColor =
+ column.name === 'status'
+ ? getStatusColor(row[column.name])
+ : '';
+ const cellValue = row[column.name] || row[column.name] === 0
+ ? column.renderer
+ ? column.renderer(row[column.name])
+ : row[column.name]
+ : '--';
- return (
-
- {cellValue}
-
- );
- })}
-
- );
- })}
-
+ return (
+
+ {cellValue}
+
+ );
+ })}
+
+ );
+ })}
+
+ {isSidePanelOpen && (
+
+ )}
+ >
);
};
@@ -168,9 +237,12 @@ import {
*/
const TransfersTable = ({ tableTitle, tableRows, totalRowCount }) => {
// get data from context
- const { pagination, setPagination, statusList, tableColumns } =
+ const { pagination, setPagination, statusList, tableColumns, sorting, setSorting } =
useTransfersContext();
+ // State to track the index of the selected row
+ const [selectedRowIndex, setSelectedRowIndex] = useState(null);
+
// pagination
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(10);
@@ -191,10 +263,72 @@ import {
const newPagination = { ...pagination, offset: newPage * rowsPerPage };
setPagination(newPagination);
};
+
+ // Sorting - initialize from context
+ const [sortBy, setSortBy] = useState(sorting?.sort_by || 'created_at');
+ const [order, setOrder] = useState(sorting?.order || 'desc');
+
+ // Sync local sorting state with context
+ useEffect(() => {
+ if (sorting) {
+ setSortBy(sorting.sort_by);
+ setOrder(sorting.order);
+ }
+ }, [sorting]);
+
+ const getColumnNames = (columnName) => {
+ let newSortBy = columnName;
+ switch (columnName) {
+ case 'created_date':
+ newSortBy = 'created_at';
+ break;
+ case 'closed_date':
+ newSortBy = 'closed_at';
+ break;
+ case 'status':
+ newSortBy = 'state';
+ break;
+ default:
+ newSortBy = columnName;
+ }
+ return newSortBy;
+ };
+
+ const mapSortBy = (columnName) => {
+ let newSortBy = getColumnNames(columnName);
+ setSortBy(newSortBy);
+ return newSortBy;
+ };
+
+ const handleSort = (column) => {
+ if (!column.sortable) return;
+
+ let newOrder = 'asc';
+
+ if (
+ (sortBy === getColumnNames(column.name) ||
+ (column.name === 'created_date' && sortBy === 'created_at') ||
+ (column.name === 'closed_date' && sortBy === 'closed_at') ||
+ (column.name === 'status' && sortBy === 'state')) &&
+ order === 'asc'
+ ) {
+ newOrder = 'desc';
+ }
+
+ setOrder(newOrder);
+
+ let newSortBy = mapSortBy(column.name);
+ setSortBy(newSortBy);
+
+ setSorting({
+ sort_by: newSortBy,
+ order: newOrder,
+ });
+ };
// get color corresponding to the status value, else default color
const getStatusColor = (status) => {
- const color = statusList.find((x) => x.value === status).color;
+ const color = statusList.find((x) => x.value === status)?.color;
return color ? color : '#585B5D';
};
@@ -217,10 +351,26 @@ import {
return (
column.sortable && handleSort(column)}
>
{column.description}
+ {column.sortable &&
+ sortBy === getColumnNames(column.name) && (
+ <>
+ {order === 'asc' && (
+
+ )}
+ {order === 'desc' && (
+
+ )}
+ >
+ )}
);
})}
@@ -230,6 +380,8 @@ import {
tableColumns={tableColumns}
tableRows={tableRows}
getStatusColor={getStatusColor}
+ selectedRowIndex={selectedRowIndex}
+ setSelectedRowIndex={setSelectedRowIndex}
/>
diff --git a/src/store/TransfersContext.js b/src/store/TransfersContext.js
index 12a77c2..6b5b98e 100644
--- a/src/store/TransfersContext.js
+++ b/src/store/TransfersContext.js
@@ -1,7 +1,10 @@
-import { createContext, useContext, useState } from 'react';
+import { createContext, useContext, useState, useEffect } from 'react';
import TransferFilter from '../models/TransferFilter';
import { formatWithCommas, getDateText } from '../utils/formatting';
import { capitalize } from '@mui/material';
+import AuthContext from './auth-context';
+import { getTransfers, getPendingTransfers } from '../api/transfers';
+import { getWallets } from '../api/wallets';
const TransfersContext = createContext();
@@ -24,16 +27,34 @@ const TransfersProvider = ({ children }) => {
});
const [filter, setFilter] = useState(defaultFilter);
- // Loader
+ const defaultSorting = {
+ sort_by: 'created_at',
+ order: 'desc',
+ };
+ const [sorting, setSorting] = useState(defaultSorting);
+
const [isLoading, setIsLoading] = useState(false);
+ const [count, setCount] = useState(0);
+
+ const [refetch, setRefetch] = useState(false);
+
+ const [managedWallets, setManagedWallets] = useState([]);
+
+ const [tableRows, setTableRows] = useState([]);
+ const [totalRowCount, setTotalRowCount] = useState(null);
+ const [message, setMessage] = useState('');
+
+ const authContext = useContext(AuthContext);
+ const wallet = JSON.parse(localStorage.getItem('wallet') || '{}');
+
// transfer statuses
const statusList = [
- {
- label: 'Requested',
- value: 'requested',
- color: 'black',
- },
+ // {
+ // label: 'Requested',
+ // value: 'requested',
+ // color: 'black',
+ // },
{
label: 'Pending',
value: 'pending',
@@ -49,11 +70,11 @@ const TransfersProvider = ({ children }) => {
value: 'cancelled',
color: 'red',
},
- {
- label: 'Failed',
- value: 'failed',
- color: 'red',
- },
+ // {
+ // label: 'Failed',
+ // value: 'failed',
+ // color: 'red',
+ // },
];
// transfers table columns
@@ -61,26 +82,26 @@ const TransfersProvider = ({ children }) => {
{
description: 'Transfer ID',
name: 'transfer_id',
- sortable: true,
+ sortable: false,
showInfoIcon: false,
},
{
description: 'Sender Wallet',
name: 'sender_wallet',
- sortable: true,
+ sortable: false,
showInfoIcon: false,
},
{
description: 'Token Amount',
name: 'token_amount',
- sortable: true,
+ sortable: false,
showInfoIcon: false,
renderer: (val) => formatWithCommas(val),
},
{
description: 'Receiver Wallet',
name: 'receiver_wallet',
- sortable: true,
+ sortable: false,
showInfoIcon: false,
},
{
@@ -93,7 +114,7 @@ const TransfersProvider = ({ children }) => {
{
description: 'Initiated By',
name: 'initiated_by',
- sortable: true,
+ sortable: false,
showInfoIcon: false,
},
{
@@ -118,17 +139,127 @@ const TransfersProvider = ({ children }) => {
return returnedRows.map(row => {
return {
transfer_id: row.id,
+ id: row.id,
sender_wallet: row.source_wallet,
token_amount: row.token_count,
receiver_wallet: row.destination_wallet,
created_date: row.created_at,
+ created_at: row.created_at,
initiated_by: row.originating_wallet,
closed_date: row.closed_at,
+ closed_at: row.closed_at,
status: row.state,
+ state: row.state,
+ source_wallet: row.source_wallet,
+ destination_wallet: row.destination_wallet,
+ originating_wallet: row.originating_wallet,
+ token_count: row.token_count,
};
});
};
+ const getStatusPriority = (status) => {
+ switch (status) {
+ case 'pending':
+ return 1;
+ case 'completed':
+ return 2;
+ case 'cancelled':
+ return 3;
+ default:
+ return 4;
+ }
+ };
+
+ const sortRowsByDefaultOrder = (rows) => {
+ return [...rows].sort((a, b) => {
+ const statusPriorityA = getStatusPriority(a.status);
+ const statusPriorityB = getStatusPriority(b.status);
+
+ if (statusPriorityA !== statusPriorityB) {
+ return statusPriorityA - statusPriorityB;
+ }
+
+ const dateA = new Date(a.created_at || a.created_date || 0);
+ const dateB = new Date(b.created_at || b.created_date || 0);
+ return dateB - dateA;
+ });
+ };
+
+ const loadData = async () => {
+ try {
+ setIsLoading(true);
+
+ const data = await getTransfers(authContext.token, {
+ pagination,
+ filter,
+ sorting,
+ });
+ let preparedRows = prepareRows(await data.transfers);
+
+ const isDefaultSort =
+ sorting.sort_by === defaultSorting.sort_by &&
+ sorting.order === defaultSorting.order;
+
+ if (isDefaultSort) {
+ preparedRows = sortRowsByDefaultOrder(preparedRows);
+ }
+
+ setTableRows(preparedRows);
+ setTotalRowCount(data.total);
+ } catch (error) {
+ console.error(error);
+ setMessage('An error occurred while fetching the table data');
+ } finally {
+ setIsLoading(false);
+ setRefetch(false);
+ }
+ };
+
+ const loadPendingTransfersData = async () => {
+ try {
+ setIsLoading(true);
+
+ const allWalletsData = await getWallets(authContext.token, '', {
+ pagination: { limit: 1000 },
+ });
+ setManagedWallets(allWalletsData);
+
+ let local_count = 0;
+ const pendingTransfers = await getPendingTransfers(authContext.token);
+
+ if (pendingTransfers && pendingTransfers.transfers) {
+ for (const item of pendingTransfers.transfers) {
+ if (wallet.name === item.destination_wallet) {
+ local_count++;
+ } else if (
+ allWalletsData.wallets &&
+ allWalletsData.wallets.some(
+ (wallet) => wallet.name === item.destination_wallet
+ )
+ ) {
+ local_count++;
+ }
+ }
+ }
+ setCount(local_count);
+ } catch (error) {
+ console.error(
+ 'An error occurred fetching the managed wallets and/or pending transfers',
+ error
+ );
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ loadPendingTransfersData();
+ }, [refetch]);
+
+ useEffect(() => {
+ loadData();
+ }, [pagination, filter, sorting, refetch]);
const value = {
pagination,
@@ -141,6 +272,17 @@ const TransfersProvider = ({ children }) => {
setIsLoading,
tableColumns,
prepareRows,
+ sorting,
+ setSorting,
+ count,
+ refetch,
+ setRefetch,
+ managedWallets,
+ tableRows,
+ totalRowCount,
+ message,
+ setMessage,
+ loadData,
};
return (
@@ -150,7 +292,6 @@ const TransfersProvider = ({ children }) => {
);
};
-// hook to return transfers context
const useTransfersContext = () => {
const context = useContext(TransfersContext);
if (!context) throw new Error('useTransfersContext must be used within TransfersProvider');