Skip to content

Commit 06e5e3a

Browse files
authored
Merge pull request #991 from PayButton/payments-filter
Feat: Payments buttons filter
2 parents 6b953d2 + b994c62 commit 06e5e3a

8 files changed

Lines changed: 348 additions & 124 deletions

File tree

assets/settings-slider-icon.png

10 KB
Loading

components/TableContainer/TableContainerGetter.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ const TableContainer = ({ columns, dataGetter, opts, ssr, tableRefreshCount, emp
7474
usePagination
7575
)
7676

77+
useEffect(() => {
78+
gotoPage(0)
79+
}, [tableRefreshCount])
80+
7781
useEffect(() => {
7882
void (async () => {
7983
setLoading(true)

pages/api/payments/count/index.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { CacheGet } from 'redis/index'
22
import { fetchUserProfileFromId } from 'services/userService'
33
import { setSession } from 'utils/setSession'
4+
import { getFilteredTransactionCount } from 'services/transactionService'
45

56
export default async (req: any, res: any): Promise<void> => {
67
if (req.method === 'GET') {
@@ -10,7 +11,20 @@ export default async (req: any, res: any): Promise<void> => {
1011
const userProfile = await fetchUserProfileFromId(userId)
1112
const userPreferredTimezone = userProfile?.preferredTimezone
1213
const timezone = userPreferredTimezone !== '' ? userPreferredTimezone : userReqTimezone
13-
const resJSON = await CacheGet.paymentsCount(userId, timezone)
14-
res.status(200).json(resJSON)
14+
15+
let buttonIds: string[] | undefined
16+
if (typeof req.query.buttonIds === 'string' && req.query.buttonIds !== '') {
17+
buttonIds = (req.query.buttonIds as string).split(',')
18+
}
19+
20+
if ((buttonIds !== undefined) && buttonIds.length > 0) {
21+
const totalCount = await getFilteredTransactionCount(userId, buttonIds)
22+
res.status(200).json(totalCount)
23+
} else {
24+
const resJSON = await CacheGet.paymentsCount(userId, timezone)
25+
res.status(200).json(resJSON)
26+
}
27+
} else {
28+
res.status(405).json({ error: 'Method not allowed' })
1529
}
1630
}

pages/api/payments/download/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,12 @@ export default async (req: any, res: any): Promise<void> => {
4343
const networkId = NETWORK_IDS[networkTicker]
4444
networkIdArray = [networkId]
4545
};
46-
const transactions = await fetchAllPaymentsByUserId(userId, networkIdArray)
46+
let buttonIds: string[] | undefined
47+
if (typeof req.query.buttonIds === 'string' && req.query.buttonIds !== '') {
48+
buttonIds = req.query.buttonIds.split(',')
49+
}
50+
const transactions = await fetchAllPaymentsByUserId(userId, networkIdArray, buttonIds)
51+
4752
await downloadTxsFile(res, quoteSlug, timezone, transactions, userId)
4853
} catch (error: any) {
4954
switch (error.message) {

pages/api/payments/index.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,19 @@ export default async (req: any, res: any): Promise<void> => {
1010
const orderDesc: boolean = !!(req.query.orderDesc === '' || req.query.orderDesc === undefined || req.query.orderDesc === 'true')
1111
const orderBy = (req.query.orderBy === '' || req.query.orderBy === undefined) ? undefined : req.query.orderBy as string
1212

13-
const resJSON = await fetchAllPaymentsByUserIdWithPagination(userId, page, pageSize, orderBy, orderDesc)
13+
let buttonIds: string[] | undefined
14+
if (typeof req.query.buttonIds === 'string' && req.query.buttonIds !== '') {
15+
buttonIds = (req.query.buttonIds as string).split(',')
16+
}
17+
18+
const resJSON = await fetchAllPaymentsByUserIdWithPagination(
19+
userId,
20+
page,
21+
pageSize,
22+
orderBy,
23+
orderDesc,
24+
buttonIds
25+
)
1426
res.status(200).json(resJSON)
1527
}
1628
}

pages/payments/index.tsx

Lines changed: 107 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import TopBar from 'components/TopBar'
1717
import { fetchUserWithSupertokens, UserWithSupertokens } from 'services/userService'
1818
import { UserProfile } from '@prisma/client'
1919
import Button from 'components/Button'
20+
import style from './payments.module.css'
21+
import SettingsIcon from '../../assets/settings-slider-icon.png'
2022

2123
export const getServerSideProps: GetServerSideProps = async (context) => {
2224
// 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
5759
const [selectedCurrencyCSV, setSelectedCurrencyCSV] = useState<string>('')
5860
const [paybuttonNetworks, setPaybuttonNetworks] = useState<Set<number>>(new Set())
5961
const [loading, setLoading] = useState(false)
62+
const [buttons, setButtons] = useState<any[]>([])
63+
const [selectedButtonIds, setSelectedButtonIds] = useState<any[]>([])
64+
const [showFilters, setShowFilters] = useState<boolean>(false)
65+
const [tableLoading, setTableLoading] = useState<boolean>(true)
66+
const [refreshCount, setRefreshCount] = useState(0)
67+
68+
useEffect(() => {
69+
setRefreshCount(prev => prev + 1)
70+
}, [selectedButtonIds])
6071

6172
const fetchPaybuttons = async (): Promise<any> => {
6273
const res = await fetch(`/api/paybuttons?userId=${user?.userProfile.id}`, {
@@ -69,6 +80,7 @@ export default function Payments ({ user, userId }: PaybuttonsProps): React.Reac
6980
const getDataAndSetUpCurrencyCSV = async (): Promise<void> => {
7081
const paybuttons = await fetchPaybuttons()
7182
const networkIds: Set<number> = new Set()
83+
setButtons(paybuttons)
7284

7385
paybuttons.forEach((p: { addresses: any[] }) => {
7486
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
8193
void getDataAndSetUpCurrencyCSV()
8294
}, [])
8395

84-
function fetchData (): Function {
85-
return async (page: number, pageSize: number, orderBy: string, orderDesc: boolean) => {
86-
const paymentsResponse = await fetch(`/api/payments?page=${page}&pageSize=${pageSize}&orderBy=${orderBy}&orderDesc=${String(orderDesc)}`)
87-
const paymentsCountResponse = await fetch('/api/payments/count', {
88-
headers: {
89-
Timezone: timezone
90-
}
91-
})
96+
const loadData = async (
97+
page: number,
98+
pageSize: number,
99+
orderBy: string,
100+
orderDesc: boolean
101+
): Promise<{ data: [], totalCount: number }> => {
102+
setTableLoading(true)
103+
try {
104+
// Build the URL including the filter if any buttons are selected
105+
let url = `/api/payments?page=${page}&pageSize=${pageSize}&orderBy=${orderBy}&orderDesc=${String(orderDesc)}`
106+
if (selectedButtonIds.length > 0) {
107+
url += `&buttonIds=${selectedButtonIds.join(',')}`
108+
}
109+
110+
const paymentsResponse = await fetch(url)
111+
const paymentsCountResponse = await fetch(
112+
`/api/payments/count${selectedButtonIds.length > 0 ? `?buttonIds=${selectedButtonIds.join(',')}` : ''}`,
113+
{ headers: { Timezone: timezone } }
114+
)
115+
116+
if (!paymentsResponse.ok || !paymentsCountResponse.ok) {
117+
console.log('paymentsResponse status', paymentsResponse.status)
118+
console.log('paymentsResponse status text', paymentsResponse.statusText)
119+
console.log('paymentsResponse body', paymentsResponse.body)
120+
console.log('paymentsResponse json', await paymentsResponse.json())
121+
throw new Error('Failed to fetch payments or count')
122+
}
123+
92124
const totalCount = await paymentsCountResponse.json()
93125
const payments = await paymentsResponse.json()
94-
return {
95-
data: payments,
96-
totalCount
97-
}
126+
127+
return { data: payments, totalCount }
128+
} catch (error) {
129+
console.error('Error fetching payments:', error)
130+
throw error
131+
} finally {
132+
setLoading(false)
98133
}
99134
}
100135

@@ -171,6 +206,9 @@ export default function Payments ({ user, userId }: PaybuttonsProps): React.Reac
171206
setLoading(true)
172207
const preferredCurrencyId = userProfile?.preferredCurrencyId ?? ''
173208
let url = `/api/payments/download/?currency=${preferredCurrencyId}`
209+
if (selectedButtonIds.length > 0) {
210+
url += `&buttonIds=${selectedButtonIds.join(',')}`
211+
}
174212
const isCurrencyEmptyOrUndefined = (value: string): boolean => (value === '' || value === undefined)
175213

176214
if (!isCurrencyEmptyOrUndefined(currency)) {
@@ -187,7 +225,18 @@ export default function Payments ({ user, userId }: PaybuttonsProps): React.Reac
187225
throw new Error('Failed to download CSV')
188226
}
189227

190-
const fileName = `${isCurrencyEmptyOrUndefined(currency) ? 'all' : `${currency.toLowerCase()}`}-transactions`
228+
const selectedButtonNames = buttons
229+
.filter(btn => selectedButtonIds.includes(btn.id))
230+
.map(btn => btn.name.replace(/\s+/g, '-'))
231+
.join('_')
232+
233+
const buttonSuffix = selectedButtonIds.length > 0
234+
? `-${selectedButtonNames !== '' ? selectedButtonNames : 'filtered'}`
235+
: ''
236+
const currencyLabel = isCurrencyEmptyOrUndefined(currency) ? 'all' : currency.toLowerCase()
237+
const timestamp = moment().format('YYYY-MM-DD_HH-mm-ss')
238+
239+
const fileName = `${currencyLabel}-transactions${buttonSuffix}-${timestamp}`
191240
const blob = await response.blob()
192241
const downloadUrl = window.URL.createObjectURL(blob)
193242
const link = document.createElement('a')
@@ -213,8 +262,24 @@ export default function Payments ({ user, userId }: PaybuttonsProps): React.Reac
213262

214263
return (
215264
<>
216-
<TopBar title="Payments" user={user?.stUser?.email} />
217-
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', justifyContent: 'right' }}>
265+
<TopBar title="Payments" user={user?.stUser?.email} />
266+
<div className={style.filters_export_ctn}>
267+
<div className={style.filter_btns}>
268+
<div
269+
onClick={() => setShowFilters(!showFilters)}
270+
className={`${style.show_filters_button} ${selectedButtonIds.length > 0 ? style.active : ''}`}
271+
>
272+
<Image src={SettingsIcon} alt="filters" width={15} />Filters
273+
</div>
274+
{selectedButtonIds.length > 0 &&
275+
<div
276+
onClick={() => setSelectedButtonIds([])}
277+
className={style.show_filters_button}
278+
>
279+
Clear
280+
</div>
281+
}
282+
</div>
218283
{paybuttonNetworks.size > 1
219284
? (
220285
<select
@@ -246,11 +311,35 @@ export default function Payments ({ user, userId }: PaybuttonsProps): React.Reac
246311
Export as CSV
247312
</Button>)}
248313
</div>
314+
{showFilters && (
315+
<div className={style.showfilters_ctn}>
316+
<span>Filter by button</span>
317+
<div className={style.filters_ctn}>
318+
{buttons.map((button) => (
319+
<div
320+
key={button.id}
321+
onClick={() => {
322+
setSelectedButtonIds(prev =>
323+
prev.includes(button.id)
324+
? prev.filter(id => id !== button.id)
325+
: [...prev, button.id]
326+
)
327+
}}
328+
className={`${style.filter_button} ${selectedButtonIds.includes(button.id) ? style.active : ''}`}
329+
>
330+
{button.name}
331+
</div>
332+
))}
333+
</div>
334+
</div>
335+
)}
249336
<TableContainerGetter
250337
columns={columns}
251-
dataGetter={fetchData()}
252-
tableRefreshCount={1}
253-
emptyMessage='No Payments to show yet'
338+
dataGetter={async (page, pageSize, orderBy, orderDesc) =>
339+
await loadData(page, pageSize, orderBy, orderDesc)
340+
}
341+
tableRefreshCount={refreshCount}
342+
emptyMessage={tableLoading ? 'Loading...' : 'No Payments to show yet'}
254343
/>
255344
</>
256345
)

pages/payments/payments.module.css

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
.filters_export_ctn {
2+
display: flex;
3+
align-items: center;
4+
gap: 50px;
5+
justify-content: space-between;
6+
}
7+
8+
.filters_export_ctn select {
9+
margin-bottom: 0;
10+
}
11+
12+
.show_filters_button {
13+
width: fit-content;
14+
padding: 4px 20px;
15+
border-radius: 6px;
16+
background-color: var(--secondary-bg-color);
17+
border: 1px solid var(--border-color);
18+
cursor: pointer;
19+
transition: all ease-in-out 200ms;
20+
font-weight: 500;
21+
user-select: none;
22+
display: flex;
23+
align-items: center;
24+
}
25+
26+
.show_filters_button img {
27+
margin-right: 6px;
28+
border-radius: 0;
29+
}
30+
31+
body[data-theme='dark'] .show_filters_button img {
32+
filter: brightness(0) invert(1);
33+
}
34+
35+
.show_filters_button:hover {
36+
background-color: var(--accent-color);
37+
}
38+
39+
.filters_ctn {
40+
display: flex;
41+
flex-wrap: wrap;
42+
gap: 5px;
43+
}
44+
45+
.filter_button {
46+
padding: 5px 15px;
47+
font-size: 12px;
48+
border-radius: 6px;
49+
background-color: var(--secondary-bg-color);
50+
cursor: pointer;
51+
transition: all ease-in-out 200ms;
52+
border: 1px solid var(--border-color);
53+
user-select: none;
54+
}
55+
56+
.filter_button:hover {
57+
border-color: var(--accent-color);
58+
}
59+
60+
.filter_button:active {
61+
transform: scale(0.95);
62+
}
63+
64+
.active {
65+
background-color: var(--accent-color);
66+
border-color: var(--accent-color);
67+
}
68+
69+
.showfilters_ctn {
70+
margin-top: 10px;
71+
margin-bottom: 20px;
72+
}
73+
74+
.showfilters_ctn span {
75+
font-size: 12px;
76+
margin: 10px 0 5px;
77+
display: inline-block;
78+
font-weight: 500;
79+
}
80+
81+
.wallet_label {
82+
margin-top: 20px !important;
83+
}
84+
85+
.filter_btns {
86+
display: flex;
87+
align-items: center;
88+
gap: 10px;
89+
}

0 commit comments

Comments
 (0)