Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added assets/settings-slider-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions components/TableContainer/TableContainerGetter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ const TableContainer = ({ columns, dataGetter, opts, ssr, tableRefreshCount, emp
usePagination
)

useEffect(() => {
gotoPage(0)
}, [tableRefreshCount])

useEffect(() => {
void (async () => {
setLoading(true)
Expand Down
18 changes: 16 additions & 2 deletions pages/api/payments/count/index.ts
Original file line number Diff line number Diff line change
@@ -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<void> => {
if (req.method === 'GET') {
Expand All @@ -10,7 +11,20 @@ export default async (req: any, res: any): Promise<void> => {
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' })
}
}
7 changes: 6 additions & 1 deletion pages/api/payments/download/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,12 @@ export default async (req: any, res: any): Promise<void> => {
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) {
Expand Down
14 changes: 13 additions & 1 deletion pages/api/payments/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,19 @@ export default async (req: any, res: any): Promise<void> => {
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)
}
}
125 changes: 107 additions & 18 deletions pages/payments/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -57,6 +59,15 @@ export default function Payments ({ user, userId }: PaybuttonsProps): React.Reac
const [selectedCurrencyCSV, setSelectedCurrencyCSV] = useState<string>('')
const [paybuttonNetworks, setPaybuttonNetworks] = useState<Set<number>>(new Set())
const [loading, setLoading] = useState(false)
const [buttons, setButtons] = useState<any[]>([])
const [selectedButtonIds, setSelectedButtonIds] = useState<any[]>([])
const [showFilters, setShowFilters] = useState<boolean>(false)
const [tableLoading, setTableLoading] = useState<boolean>(true)
const [refreshCount, setRefreshCount] = useState(0)

useEffect(() => {
setRefreshCount(prev => prev + 1)
}, [selectedButtonIds])

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

paybuttons.forEach((p: { addresses: any[] }) => {
return p.addresses.forEach((c: { address: { networkId: number } }) => networkIds.add(c.address.networkId))
Expand All @@ -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)
}
}

Expand Down Expand Up @@ -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)) {
Expand All @@ -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')
Expand All @@ -213,8 +262,24 @@ export default function Payments ({ user, userId }: PaybuttonsProps): React.Reac

return (
<>
<TopBar title="Payments" user={user?.stUser?.email} />
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', justifyContent: 'right' }}>
<TopBar title="Payments" user={user?.stUser?.email} />
<div className={style.filters_export_ctn}>
<div className={style.filter_btns}>
<div
onClick={() => setShowFilters(!showFilters)}
className={`${style.show_filters_button} ${selectedButtonIds.length > 0 ? style.active : ''}`}
>
<Image src={SettingsIcon} alt="filters" width={15} />Filters
</div>
{selectedButtonIds.length > 0 &&
<div
onClick={() => setSelectedButtonIds([])}
className={style.show_filters_button}
>
Clear
</div>
}
</div>
{paybuttonNetworks.size > 1
? (
<select
Expand Down Expand Up @@ -246,11 +311,35 @@ export default function Payments ({ user, userId }: PaybuttonsProps): React.Reac
Export as CSV
</Button>)}
</div>
{showFilters && (
<div className={style.showfilters_ctn}>
<span>Filter by button</span>
<div className={style.filters_ctn}>
{buttons.map((button) => (
<div
key={button.id}
onClick={() => {
setSelectedButtonIds(prev =>
prev.includes(button.id)
? prev.filter(id => id !== button.id)
: [...prev, button.id]
)
}}
className={`${style.filter_button} ${selectedButtonIds.includes(button.id) ? style.active : ''}`}
>
{button.name}
</div>
))}
</div>
</div>
)}
<TableContainerGetter
columns={columns}
dataGetter={fetchData()}
tableRefreshCount={1}
emptyMessage='No Payments to show yet'
dataGetter={async (page, pageSize, orderBy, orderDesc) =>
await loadData(page, pageSize, orderBy, orderDesc)
}
tableRefreshCount={refreshCount}
emptyMessage={tableLoading ? 'Loading...' : 'No Payments to show yet'}
/>
</>
)
Expand Down
89 changes: 89 additions & 0 deletions pages/payments/payments.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
.filters_export_ctn {
display: flex;
align-items: center;
gap: 50px;
justify-content: space-between;
}

.filters_export_ctn select {
margin-bottom: 0;
}

.show_filters_button {
width: fit-content;
padding: 4px 20px;
border-radius: 6px;
background-color: var(--secondary-bg-color);
border: 1px solid var(--border-color);
cursor: pointer;
transition: all ease-in-out 200ms;
font-weight: 500;
user-select: none;
display: flex;
align-items: center;
}

.show_filters_button img {
margin-right: 6px;
border-radius: 0;
}

body[data-theme='dark'] .show_filters_button img {
filter: brightness(0) invert(1);
}

.show_filters_button:hover {
background-color: var(--accent-color);
}

.filters_ctn {
display: flex;
flex-wrap: wrap;
gap: 5px;
}

.filter_button {
padding: 5px 15px;
font-size: 12px;
border-radius: 6px;
background-color: var(--secondary-bg-color);
cursor: pointer;
transition: all ease-in-out 200ms;
border: 1px solid var(--border-color);
user-select: none;
}

.filter_button:hover {
border-color: var(--accent-color);
}

.filter_button:active {
transform: scale(0.95);
}

.active {
background-color: var(--accent-color);
border-color: var(--accent-color);
}

.showfilters_ctn {
margin-top: 10px;
margin-bottom: 20px;
}

.showfilters_ctn span {
font-size: 12px;
margin: 10px 0 5px;
display: inline-block;
font-weight: 500;
}

.wallet_label {
margin-top: 20px !important;
}

.filter_btns {
display: flex;
align-items: center;
gap: 10px;
}
Loading