diff --git a/assets/settings-slider-icon.png b/assets/settings-slider-icon.png new file mode 100644 index 000000000..4a5eff295 Binary files /dev/null and b/assets/settings-slider-icon.png differ diff --git a/components/TableContainer/TableContainerGetter.tsx b/components/TableContainer/TableContainerGetter.tsx index 5eb2bd0b7..978c73a66 100644 --- a/components/TableContainer/TableContainerGetter.tsx +++ b/components/TableContainer/TableContainerGetter.tsx @@ -74,6 +74,10 @@ const TableContainer = ({ columns, dataGetter, opts, ssr, tableRefreshCount, emp usePagination ) + useEffect(() => { + gotoPage(0) + }, [tableRefreshCount]) + useEffect(() => { void (async () => { setLoading(true) diff --git a/pages/api/payments/count/index.ts b/pages/api/payments/count/index.ts index 96c3eb1be..eb158d821 100644 --- a/pages/api/payments/count/index.ts +++ b/pages/api/payments/count/index.ts @@ -1,6 +1,7 @@ import { CacheGet } from 'redis/index' import { fetchUserProfileFromId } from 'services/userService' import { setSession } from 'utils/setSession' +import { getFilteredTransactionCount } from 'services/transactionService' export default async (req: any, res: any): Promise => { if (req.method === 'GET') { @@ -10,7 +11,20 @@ export default async (req: any, res: any): Promise => { const userProfile = await fetchUserProfileFromId(userId) const userPreferredTimezone = userProfile?.preferredTimezone const timezone = userPreferredTimezone !== '' ? userPreferredTimezone : userReqTimezone - const resJSON = await CacheGet.paymentsCount(userId, timezone) - res.status(200).json(resJSON) + + let buttonIds: string[] | undefined + if (typeof req.query.buttonIds === 'string' && req.query.buttonIds !== '') { + buttonIds = (req.query.buttonIds as string).split(',') + } + + if ((buttonIds !== undefined) && buttonIds.length > 0) { + const totalCount = await getFilteredTransactionCount(userId, buttonIds) + res.status(200).json(totalCount) + } else { + const resJSON = await CacheGet.paymentsCount(userId, timezone) + res.status(200).json(resJSON) + } + } else { + res.status(405).json({ error: 'Method not allowed' }) } } diff --git a/pages/api/payments/download/index.ts b/pages/api/payments/download/index.ts index b1707d5f0..4bab708b6 100644 --- a/pages/api/payments/download/index.ts +++ b/pages/api/payments/download/index.ts @@ -43,7 +43,12 @@ export default async (req: any, res: any): Promise => { const networkId = NETWORK_IDS[networkTicker] networkIdArray = [networkId] }; - const transactions = await fetchAllPaymentsByUserId(userId, networkIdArray) + let buttonIds: string[] | undefined + if (typeof req.query.buttonIds === 'string' && req.query.buttonIds !== '') { + buttonIds = req.query.buttonIds.split(',') + } + const transactions = await fetchAllPaymentsByUserId(userId, networkIdArray, buttonIds) + await downloadTxsFile(res, quoteSlug, timezone, transactions, userId) } catch (error: any) { switch (error.message) { diff --git a/pages/api/payments/index.ts b/pages/api/payments/index.ts index 84215e557..d626f9732 100644 --- a/pages/api/payments/index.ts +++ b/pages/api/payments/index.ts @@ -10,7 +10,19 @@ export default async (req: any, res: any): Promise => { const orderDesc: boolean = !!(req.query.orderDesc === '' || req.query.orderDesc === undefined || req.query.orderDesc === 'true') const orderBy = (req.query.orderBy === '' || req.query.orderBy === undefined) ? undefined : req.query.orderBy as string - const resJSON = await fetchAllPaymentsByUserIdWithPagination(userId, page, pageSize, orderBy, orderDesc) + let buttonIds: string[] | undefined + if (typeof req.query.buttonIds === 'string' && req.query.buttonIds !== '') { + buttonIds = (req.query.buttonIds as string).split(',') + } + + const resJSON = await fetchAllPaymentsByUserIdWithPagination( + userId, + page, + pageSize, + orderBy, + orderDesc, + buttonIds + ) res.status(200).json(resJSON) } } diff --git a/pages/payments/index.tsx b/pages/payments/index.tsx index 6785a8f96..5d725a018 100644 --- a/pages/payments/index.tsx +++ b/pages/payments/index.tsx @@ -17,6 +17,8 @@ import TopBar from 'components/TopBar' import { fetchUserWithSupertokens, UserWithSupertokens } from 'services/userService' import { UserProfile } from '@prisma/client' import Button from 'components/Button' +import style from './payments.module.css' +import SettingsIcon from '../../assets/settings-slider-icon.png' export const getServerSideProps: GetServerSideProps = async (context) => { // this runs on the backend, so we must call init on supertokens-node SDK @@ -57,6 +59,15 @@ export default function Payments ({ user, userId }: PaybuttonsProps): React.Reac const [selectedCurrencyCSV, setSelectedCurrencyCSV] = useState('') const [paybuttonNetworks, setPaybuttonNetworks] = useState>(new Set()) const [loading, setLoading] = useState(false) + const [buttons, setButtons] = useState([]) + const [selectedButtonIds, setSelectedButtonIds] = useState([]) + const [showFilters, setShowFilters] = useState(false) + const [tableLoading, setTableLoading] = useState(true) + const [refreshCount, setRefreshCount] = useState(0) + + useEffect(() => { + setRefreshCount(prev => prev + 1) + }, [selectedButtonIds]) const fetchPaybuttons = async (): Promise => { const res = await fetch(`/api/paybuttons?userId=${user?.userProfile.id}`, { @@ -69,6 +80,7 @@ export default function Payments ({ user, userId }: PaybuttonsProps): React.Reac const getDataAndSetUpCurrencyCSV = async (): Promise => { const paybuttons = await fetchPaybuttons() const networkIds: Set = new Set() + setButtons(paybuttons) paybuttons.forEach((p: { addresses: any[] }) => { return p.addresses.forEach((c: { address: { networkId: number } }) => networkIds.add(c.address.networkId)) @@ -81,20 +93,43 @@ export default function Payments ({ user, userId }: PaybuttonsProps): React.Reac void getDataAndSetUpCurrencyCSV() }, []) - function fetchData (): Function { - return async (page: number, pageSize: number, orderBy: string, orderDesc: boolean) => { - const paymentsResponse = await fetch(`/api/payments?page=${page}&pageSize=${pageSize}&orderBy=${orderBy}&orderDesc=${String(orderDesc)}`) - const paymentsCountResponse = await fetch('/api/payments/count', { - headers: { - Timezone: timezone - } - }) + const loadData = async ( + page: number, + pageSize: number, + orderBy: string, + orderDesc: boolean + ): Promise<{ data: [], totalCount: number }> => { + setTableLoading(true) + try { + // Build the URL including the filter if any buttons are selected + let url = `/api/payments?page=${page}&pageSize=${pageSize}&orderBy=${orderBy}&orderDesc=${String(orderDesc)}` + if (selectedButtonIds.length > 0) { + url += `&buttonIds=${selectedButtonIds.join(',')}` + } + + const paymentsResponse = await fetch(url) + const paymentsCountResponse = await fetch( + `/api/payments/count${selectedButtonIds.length > 0 ? `?buttonIds=${selectedButtonIds.join(',')}` : ''}`, + { headers: { Timezone: timezone } } + ) + + if (!paymentsResponse.ok || !paymentsCountResponse.ok) { + console.log('paymentsResponse status', paymentsResponse.status) + console.log('paymentsResponse status text', paymentsResponse.statusText) + console.log('paymentsResponse body', paymentsResponse.body) + console.log('paymentsResponse json', await paymentsResponse.json()) + throw new Error('Failed to fetch payments or count') + } + const totalCount = await paymentsCountResponse.json() const payments = await paymentsResponse.json() - return { - data: payments, - totalCount - } + + return { data: payments, totalCount } + } catch (error) { + console.error('Error fetching payments:', error) + throw error + } finally { + setLoading(false) } } @@ -171,6 +206,9 @@ export default function Payments ({ user, userId }: PaybuttonsProps): React.Reac setLoading(true) const preferredCurrencyId = userProfile?.preferredCurrencyId ?? '' let url = `/api/payments/download/?currency=${preferredCurrencyId}` + if (selectedButtonIds.length > 0) { + url += `&buttonIds=${selectedButtonIds.join(',')}` + } const isCurrencyEmptyOrUndefined = (value: string): boolean => (value === '' || value === undefined) if (!isCurrencyEmptyOrUndefined(currency)) { @@ -187,7 +225,18 @@ export default function Payments ({ user, userId }: PaybuttonsProps): React.Reac throw new Error('Failed to download CSV') } - const fileName = `${isCurrencyEmptyOrUndefined(currency) ? 'all' : `${currency.toLowerCase()}`}-transactions` + const selectedButtonNames = buttons + .filter(btn => selectedButtonIds.includes(btn.id)) + .map(btn => btn.name.replace(/\s+/g, '-')) + .join('_') + + const buttonSuffix = selectedButtonIds.length > 0 + ? `-${selectedButtonNames !== '' ? selectedButtonNames : 'filtered'}` + : '' + const currencyLabel = isCurrencyEmptyOrUndefined(currency) ? 'all' : currency.toLowerCase() + const timestamp = moment().format('YYYY-MM-DD_HH-mm-ss') + + const fileName = `${currencyLabel}-transactions${buttonSuffix}-${timestamp}` const blob = await response.blob() const downloadUrl = window.URL.createObjectURL(blob) const link = document.createElement('a') @@ -213,8 +262,24 @@ export default function Payments ({ user, userId }: PaybuttonsProps): React.Reac return ( <> - -
+ +
+
+
setShowFilters(!showFilters)} + className={`${style.show_filters_button} ${selectedButtonIds.length > 0 ? style.active : ''}`} + > + filtersFilters +
+ {selectedButtonIds.length > 0 && +
setSelectedButtonIds([])} + className={style.show_filters_button} + > + Clear +
+ } +
{paybuttonNetworks.size > 1 ? (