From 07dea23187ba522db36e9582bacc6852bc6dec2c Mon Sep 17 00:00:00 2001 From: lissavxo Date: Thu, 19 Jun 2025 11:57:02 -0300 Subject: [PATCH 1/6] feat: get transaction years --- pages/api/transaction/years/index.ts | 15 +++++++++++++++ services/transactionService.ts | 17 +++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 pages/api/transaction/years/index.ts diff --git a/pages/api/transaction/years/index.ts b/pages/api/transaction/years/index.ts new file mode 100644 index 000000000..ad5f51caf --- /dev/null +++ b/pages/api/transaction/years/index.ts @@ -0,0 +1,15 @@ +import { fetchDistinctTransactionYearsByUser } from 'services/transactionService' +import { setSession } from 'utils/setSession' + +export default async (req: any, res: any): Promise => { + if (req.method === 'GET') { + try { + await setSession(req, res) + const userId = req.session.userId + const years = await fetchDistinctTransactionYearsByUser(userId) + res.status(200).json({ years }) + } catch (err: any) { + res.status(500).json({ statusCode: 500, message: err.message }) + } + } +} diff --git a/services/transactionService.ts b/services/transactionService.ts index 57a03c06c..4baf92499 100644 --- a/services/transactionService.ts +++ b/services/transactionService.ts @@ -587,6 +587,10 @@ export async function fetchTransactionsByPaybuttonIdWithPagination ( orderDesc, networkIds) + if (transactions.length === 0) { + throw new Error(RESPONSE_MESSAGES.NO_TRANSACTION_FOUND_404.message) + } + return transactions } @@ -898,3 +902,16 @@ export const getFilteredTransactionCount = async ( } }) } + +export async function fetchDistinctTransactionYearsByUser (userId: string): Promise { + const years = await prisma.$queryRaw>` + SELECT DISTINCT YEAR(FROM_UNIXTIME(t.timestamp)) AS year + FROM Transaction t + JOIN Address a ON a.id = t.addressId + JOIN AddressesOnUserProfiles ap ON ap.addressId = a.id + WHERE ap.userId = ${userId} + ORDER BY year ASC + ` + + return years.map(y => y.year) +} From fdb74dc06d9680361a099e1a661e8e765c62bc5a Mon Sep 17 00:00:00 2001 From: lissavxo Date: Thu, 3 Jul 2025 13:00:20 -0300 Subject: [PATCH 2/6] feat: add years filter api --- pages/api/payments/count/index.ts | 10 ++-- pages/api/payments/index.ts | 7 ++- pages/api/transaction/years/index.ts | 4 +- services/transactionService.ts | 69 +++++++++++++++++++++------- 4 files changed, 67 insertions(+), 23 deletions(-) diff --git a/pages/api/payments/count/index.ts b/pages/api/payments/count/index.ts index 51b9a3fe4..c60cd187c 100644 --- a/pages/api/payments/count/index.ts +++ b/pages/api/payments/count/index.ts @@ -16,9 +16,13 @@ export default async (req: any, res: any): Promise => { 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) + let years: string[] | undefined + if (typeof req.query.years === 'string' && req.query.years !== '') { + years = (req.query.years as string).split(',') + } + if (((buttonIds !== undefined) && buttonIds.length > 0) || + ((years !== undefined) && years.length > 0)) { + const totalCount = await getFilteredTransactionCount(userId, buttonIds, years) res.status(200).json(totalCount) } else { const totalCount = await CacheGet.paymentsCount(userId, timezone) diff --git a/pages/api/payments/index.ts b/pages/api/payments/index.ts index d626f9732..f8ebd308f 100644 --- a/pages/api/payments/index.ts +++ b/pages/api/payments/index.ts @@ -14,6 +14,10 @@ export default async (req: any, res: any): Promise => { if (typeof req.query.buttonIds === 'string' && req.query.buttonIds !== '') { buttonIds = (req.query.buttonIds as string).split(',') } + let years: string[] | undefined + if (typeof req.query.years === 'string' && req.query.years !== '') { + years = (req.query.years as string).split(',') + } const resJSON = await fetchAllPaymentsByUserIdWithPagination( userId, @@ -21,7 +25,8 @@ export default async (req: any, res: any): Promise => { pageSize, orderBy, orderDesc, - buttonIds + buttonIds, + years ) res.status(200).json(resJSON) } diff --git a/pages/api/transaction/years/index.ts b/pages/api/transaction/years/index.ts index ad5f51caf..2ea7782e4 100644 --- a/pages/api/transaction/years/index.ts +++ b/pages/api/transaction/years/index.ts @@ -1,4 +1,4 @@ -import { fetchDistinctTransactionYearsByUser } from 'services/transactionService' +import { fetchDistinctPaymentYearsByUser } from 'services/transactionService' import { setSession } from 'utils/setSession' export default async (req: any, res: any): Promise => { @@ -6,7 +6,7 @@ export default async (req: any, res: any): Promise => { try { await setSession(req, res) const userId = req.session.userId - const years = await fetchDistinctTransactionYearsByUser(userId) + const years = await fetchDistinctPaymentYearsByUser(userId) res.status(200).json({ years }) } catch (err: any) { res.status(500).json({ statusCode: 500, message: err.message }) diff --git a/services/transactionService.ts b/services/transactionService.ts index 4baf92499..99b2e7461 100644 --- a/services/transactionService.ts +++ b/services/transactionService.ts @@ -759,7 +759,8 @@ export async function fetchAllPaymentsByUserIdWithPagination ( pageSize: number, orderBy?: string, orderDesc = true, - buttonIds?: string[] + buttonIds?: string[], + years?: string[] ): Promise { const orderDescString: Prisma.SortOrder = orderDesc ? 'desc' : 'asc' @@ -802,6 +803,20 @@ export async function fetchAllPaymentsByUserIdWithPagination ( gt: 0 } } + if (years !== undefined && years.length > 0) { + const yearFilters = years.map((year) => { + const start = new Date(`${year}-01-01T00:00:00Z`).getTime() / 1000 + const end = new Date(`${Number(year) + 1}-01-01T00:00:00Z`).getTime() / 1000 + return { + timestamp: { + gte: Math.floor(start), + lt: Math.floor(end) + } + } + }) + + where.OR = yearFilters + } if ((buttonIds !== undefined) && buttonIds.length > 0) { where.address!.paybuttons = { @@ -884,32 +899,52 @@ export async function fetchTxCountByPaybuttonId (paybuttonId: string): Promise => { - return await prisma.transaction.count({ - where: { - address: { - userProfiles: { - some: { userId } - }, - paybuttons: { - some: { - paybutton: { id: { in: buttonIds } } - } + const where: Prisma.TransactionWhereInput = { + address: { + userProfiles: { + some: { userId } + } + }, + amount: { gt: 0 } + } + if (buttonIds !== undefined && buttonIds.length > 0) { + where.address!.paybuttons = { + some: { + paybutton: { + id: { in: buttonIds } } - }, - amount: { gt: 0 } + } } - }) + } + if (years !== undefined && years.length > 0) { + const yearFilters = years.map((year) => { + const start = new Date(`${year}-01-01T00:00:00Z`).getTime() / 1000 + const end = new Date(`${Number(year) + 1}-01-01T00:00:00Z`).getTime() / 1000 + return { + timestamp: { + gte: Math.floor(start), + lt: Math.floor(end) + } + } + }) + + where.OR = yearFilters + } + + return await prisma.transaction.count({ where }) } -export async function fetchDistinctTransactionYearsByUser (userId: string): Promise { +export const fetchDistinctPaymentYearsByUser = async (userId: string): Promise => { const years = await prisma.$queryRaw>` SELECT DISTINCT YEAR(FROM_UNIXTIME(t.timestamp)) AS year FROM Transaction t JOIN Address a ON a.id = t.addressId JOIN AddressesOnUserProfiles ap ON ap.addressId = a.id - WHERE ap.userId = ${userId} + WHERE ap.userId = ${userId} AND + t.amount > 0 ORDER BY year ASC ` From fb93cc46e2c8cdd2963955d5c14d180beca6d6b8 Mon Sep 17 00:00:00 2001 From: lissavxo Date: Thu, 3 Jul 2025 13:01:03 -0300 Subject: [PATCH 3/6] feat: years filter paymentes page --- pages/payments/index.tsx | 62 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 57 insertions(+), 5 deletions(-) diff --git a/pages/payments/index.tsx b/pages/payments/index.tsx index 21158dff5..32fc657bf 100644 --- a/pages/payments/index.tsx +++ b/pages/payments/index.tsx @@ -71,6 +71,9 @@ export default function Payments ({ user, userId, organization }: PaybuttonsProp const timezone = user?.userProfile.preferredTimezone === '' ? moment.tz.guess() : user?.userProfile?.preferredTimezone const [selectedCurrencyCSV, setSelectedCurrencyCSV] = useState('') const [paybuttonNetworks, setPaybuttonNetworks] = useState>(new Set()) + const [transactionYears, setTransactionYears] = useState([]) + const [selectedTransactionYears, setSelectedTransactionYears] = useState([]) + const [loading, setLoading] = useState(false) const [buttons, setButtons] = useState([]) const [selectedButtonIds, setSelectedButtonIds] = useState([]) @@ -130,7 +133,7 @@ export default function Payments ({ user, userId, organization }: PaybuttonsProp } useEffect(() => { setRefreshCount(prev => prev + 1) - }, [selectedButtonIds]) + }, [selectedButtonIds, selectedTransactionYears]) const fetchPaybuttons = async (): Promise => { const res = await fetch(`/api/paybuttons?userId=${user?.userProfile.id}`, { @@ -141,8 +144,21 @@ export default function Payments ({ user, userId, organization }: PaybuttonsProp } } + const fetchTransactionYears = async (): Promise => { + const res = await fetch('/api/transaction/years', { + method: 'GET' + }) + if (res.status === 200) { + const data = await res.json() + return data.years + } else { + console.error('Failed to fetch transaction years:', res.statusText) + return [] + } + } const getDataAndSetUpCurrencyCSV = async (): Promise => { const paybuttons = await fetchPaybuttons() + const years = await fetchTransactionYears() const networkIds: Set = new Set() setButtons(paybuttons) @@ -151,6 +167,7 @@ export default function Payments ({ user, userId, organization }: PaybuttonsProp }) setPaybuttonNetworks(networkIds) + setTransactionYears(years) } useEffect(() => { @@ -170,10 +187,20 @@ export default function Payments ({ user, userId, organization }: PaybuttonsProp if (selectedButtonIds.length > 0) { url += `&buttonIds=${selectedButtonIds.join(',')}` } + if (selectedTransactionYears.length > 0) { + url += `&years=${selectedTransactionYears.join(',')}` + } const paymentsResponse = await fetch(url) + let paymentsCountUrl = '/api/payments/count' + if (selectedButtonIds.length > 0) { + paymentsCountUrl += `?buttonIds=${selectedButtonIds.join(',')}` + } + if (selectedTransactionYears.length > 0) { + paymentsCountUrl += `${selectedButtonIds.length > 0 ? '&' : '?'}years=${selectedTransactionYears.join(',')}` + } const paymentsCountResponse = await fetch( - `/api/payments/count${selectedButtonIds.length > 0 ? `?buttonIds=${selectedButtonIds.join(',')}` : ''}`, + paymentsCountUrl, { headers: { Timezone: timezone } } ) @@ -403,7 +430,10 @@ export default function Payments ({ user, userId, organization }: PaybuttonsProp setSelectedCurrencyCSV(currencyParam) void downloadCSV(userId, user?.userProfile, currencyParam) } - + const handleClearFilters = (): void => { + setSelectedButtonIds([]) + setSelectedTransactionYears([]) + } return ( <> @@ -415,9 +445,9 @@ export default function Payments ({ user, userId, organization }: PaybuttonsProp > filtersFilters - {selectedButtonIds.length > 0 && + {(selectedButtonIds.length > 0 || selectedTransactionYears.length > 0) &&
setSelectedButtonIds([])} + onClick={() => handleClearFilters()} className={style.show_filters_button} > Clear @@ -456,6 +486,7 @@ export default function Payments ({ user, userId, organization }: PaybuttonsProp )}
{showFilters && ( +
Filter by button
@@ -476,6 +507,27 @@ export default function Payments ({ user, userId, organization }: PaybuttonsProp ))}
+
+ Filter by year +
+ {transactionYears.map((y) => ( +
{ + setSelectedTransactionYears(prev => + prev.includes(y) + ? prev.filter(year => year !== y) + : [...prev, y] + ) + }} + className={`${style.filter_button} ${selectedTransactionYears.includes(y) ? style.active : ''}`} + > + {y} +
+ ))} +
+
+
)} Date: Thu, 3 Jul 2025 16:35:08 -0300 Subject: [PATCH 4/6] feat: years filter on csv --- pages/api/payments/download/index.ts | 7 ++++++- pages/payments/index.tsx | 3 +++ services/transactionService.ts | 18 +++++++++++++++++- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/pages/api/payments/download/index.ts b/pages/api/payments/download/index.ts index 4bab708b6..e2bf7457f 100644 --- a/pages/api/payments/download/index.ts +++ b/pages/api/payments/download/index.ts @@ -47,7 +47,12 @@ export default async (req: any, res: any): Promise => { if (typeof req.query.buttonIds === 'string' && req.query.buttonIds !== '') { buttonIds = req.query.buttonIds.split(',') } - const transactions = await fetchAllPaymentsByUserId(userId, networkIdArray, buttonIds) + let years: string[] | undefined + if (typeof req.query.years === 'string' && req.query.years !== '') { + years = (req.query.years as string).split(',') + } + + const transactions = await fetchAllPaymentsByUserId(userId, networkIdArray, buttonIds, years) await downloadTxsFile(res, quoteSlug, timezone, transactions, userId) } catch (error: any) { diff --git a/pages/payments/index.tsx b/pages/payments/index.tsx index 32fc657bf..500f1bf44 100644 --- a/pages/payments/index.tsx +++ b/pages/payments/index.tsx @@ -380,6 +380,9 @@ export default function Payments ({ user, userId, organization }: PaybuttonsProp if (selectedButtonIds.length > 0) { url += `&buttonIds=${selectedButtonIds.join(',')}` } + if (selectedTransactionYears.length > 0) { + url += `&years=${selectedTransactionYears.join(',')}` + } const isCurrencyEmptyOrUndefined = (value: string): boolean => (value === '' || value === undefined) if (!isCurrencyEmptyOrUndefined(currency)) { diff --git a/services/transactionService.ts b/services/transactionService.ts index 99b2e7461..d7bae6ee2 100644 --- a/services/transactionService.ts +++ b/services/transactionService.ts @@ -850,7 +850,8 @@ export async function fetchAllPaymentsByUserIdWithPagination ( export async function fetchAllPaymentsByUserId ( userId: string, networkIds?: number[], - buttonIds?: string[] + buttonIds?: string[], + years?: string[] ): Promise { const where: Prisma.TransactionWhereInput = { address: { @@ -876,6 +877,21 @@ export async function fetchAllPaymentsByUserId ( } } + if (years !== undefined && years.length > 0) { + const yearFilters = years.map((year) => { + const start = new Date(`${year}-01-01T00:00:00Z`).getTime() / 1000 + const end = new Date(`${Number(year) + 1}-01-01T00:00:00Z`).getTime() / 1000 + return { + timestamp: { + gte: Math.floor(start), + lt: Math.floor(end) + } + } + }) + + where.OR = yearFilters + } + return await prisma.transaction.findMany({ where, include: includePaybuttonsAndPrices, From 3cdce41a0b4068985efa66ecb9c8469a76952b57 Mon Sep 17 00:00:00 2001 From: lissavxo Date: Mon, 28 Jul 2025 18:06:22 -0300 Subject: [PATCH 5/6] refactor: copilot suggestions --- components/Transaction/Invoice.tsx | 18 ++++++++++++++++-- components/Transaction/InvoiceModal.tsx | 1 - pages/payments/index.tsx | 2 +- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/components/Transaction/Invoice.tsx b/components/Transaction/Invoice.tsx index f4d241d92..a3cdfd388 100644 --- a/components/Transaction/Invoice.tsx +++ b/components/Transaction/Invoice.tsx @@ -3,8 +3,22 @@ import { XEC_TX_EXPLORER_URL, BCH_TX_EXPLORER_URL, NETWORK_TICKERS_FROM_ID, XEC_ import moment from 'moment' import logoImageSource from 'assets/logo.png' import Image from 'next/image' - -const Receipt = React.forwardRef((props, ref) => { +interface ReceiptProps { + data: { + invoiceNumber: string + amount: number + recipientName: string + recipientAddress: string + description: string + customerName: string + customerAddress: string + createdAt: string | number | Date + transactionHash: string + transactionDate: number + transactionNetworkId: number + } +} +const Receipt = React.forwardRef((props, ref) => { const { data } = props const { invoiceNumber, diff --git a/components/Transaction/InvoiceModal.tsx b/components/Transaction/InvoiceModal.tsx index faef8fe7b..a4731c43d 100644 --- a/components/Transaction/InvoiceModal.tsx +++ b/components/Transaction/InvoiceModal.tsx @@ -241,7 +241,6 @@ export default function InvoiceModal ({ }
- {/*
*/}
diff --git a/pages/payments/index.tsx b/pages/payments/index.tsx index 500f1bf44..32c7c4aff 100644 --- a/pages/payments/index.tsx +++ b/pages/payments/index.tsx @@ -336,7 +336,7 @@ export default function Payments ({ user, userId, organization }: PaybuttonsProp create invoice -
New button
+
New Invoice
) : ( From 777a029815c3ee04e06265aad48fe3a8016bf736 Mon Sep 17 00:00:00 2001 From: lissavxo Date: Mon, 11 Aug 2025 16:15:36 -0300 Subject: [PATCH 6/6] refactor: clean code --- pages/api/payments/index.ts | 7 +++- pages/payments/index.tsx | 6 ++- services/transactionService.ts | 76 ++++++++++++++++------------------ 3 files changed, 46 insertions(+), 43 deletions(-) diff --git a/pages/api/payments/index.ts b/pages/api/payments/index.ts index f8ebd308f..43a5d66f4 100644 --- a/pages/api/payments/index.ts +++ b/pages/api/payments/index.ts @@ -1,4 +1,5 @@ import { fetchAllPaymentsByUserIdWithPagination } from 'services/transactionService' +import { fetchUserProfileFromId } from 'services/userService' import { setSession } from 'utils/setSession' export default async (req: any, res: any): Promise => { @@ -18,6 +19,9 @@ export default async (req: any, res: any): Promise => { if (typeof req.query.years === 'string' && req.query.years !== '') { years = (req.query.years as string).split(',') } + const userReqTimezone = req.headers.timezone as string + const userProfile = await fetchUserProfileFromId(userId) + const userPreferredTimezone = userProfile?.preferredTimezone const resJSON = await fetchAllPaymentsByUserIdWithPagination( userId, @@ -26,7 +30,8 @@ export default async (req: any, res: any): Promise => { orderBy, orderDesc, buttonIds, - years + years, + userPreferredTimezone ?? userReqTimezone ) res.status(200).json(resJSON) } diff --git a/pages/payments/index.tsx b/pages/payments/index.tsx index 32c7c4aff..e85d54184 100644 --- a/pages/payments/index.tsx +++ b/pages/payments/index.tsx @@ -191,7 +191,11 @@ export default function Payments ({ user, userId, organization }: PaybuttonsProp url += `&years=${selectedTransactionYears.join(',')}` } - const paymentsResponse = await fetch(url) + const paymentsResponse = await fetch(url, { + headers: { + Timezone: moment.tz.guess() + } + }) let paymentsCountUrl = '/api/payments/count' if (selectedButtonIds.length > 0) { paymentsCountUrl += `?buttonIds=${selectedButtonIds.join(',')}` diff --git a/services/transactionService.ts b/services/transactionService.ts index d7bae6ee2..98bcd44c1 100644 --- a/services/transactionService.ts +++ b/services/transactionService.ts @@ -564,10 +564,6 @@ export async function fetchTransactionsByPaybuttonId (paybuttonId: string, netwo const addressIdList = await fetchAddressesByPaybuttonId(paybuttonId) const transactions = await fetchTransactionsByAddressList(addressIdList, networkIds) - if (transactions.length === 0) { - throw new Error(RESPONSE_MESSAGES.NO_TRANSACTION_FOUND_404.message) - } - return transactions } @@ -587,10 +583,6 @@ export async function fetchTransactionsByPaybuttonIdWithPagination ( orderDesc, networkIds) - if (transactions.length === 0) { - throw new Error(RESPONSE_MESSAGES.NO_TRANSACTION_FOUND_404.message) - } - return transactions } @@ -760,7 +752,8 @@ export async function fetchAllPaymentsByUserIdWithPagination ( orderBy?: string, orderDesc = true, buttonIds?: string[], - years?: string[] + years?: string[], + timezone?: string ): Promise { const orderDescString: Prisma.SortOrder = orderDesc ? 'desc' : 'asc' @@ -804,16 +797,7 @@ export async function fetchAllPaymentsByUserIdWithPagination ( } } if (years !== undefined && years.length > 0) { - const yearFilters = years.map((year) => { - const start = new Date(`${year}-01-01T00:00:00Z`).getTime() / 1000 - const end = new Date(`${Number(year) + 1}-01-01T00:00:00Z`).getTime() / 1000 - return { - timestamp: { - gte: Math.floor(start), - lt: Math.floor(end) - } - } - }) + const yearFilters = getYearFilters(years, timezone) where.OR = yearFilters } @@ -846,12 +830,39 @@ export async function fetchAllPaymentsByUserIdWithPagination ( } return transformedData } +const getYearFilters = (years: string[], timezone?: string): Prisma.TransactionWhereInput[] => { + return years.map((year) => { + let start: number + let end: number + if (timezone !== undefined && timezone !== null && timezone !== '') { + const startDate = new Date(`${year}-01-01T00:00:00`) + const endDate = new Date(`${Number(year) + 1}-01-01T00:00:00`) + const startInTimezone = new Date(startDate.toLocaleString('en-US', { timeZone: timezone })) + const endInTimezone = new Date(endDate.toLocaleString('en-US', { timeZone: timezone })) + const startOffset = startDate.getTime() - startInTimezone.getTime() + const endOffset = endDate.getTime() - endInTimezone.getTime() + start = (startDate.getTime() + startOffset) / 1000 + end = (endDate.getTime() + endOffset) / 1000 + } else { + start = new Date(`${year}-01-01T00:00:00Z`).getTime() / 1000 + end = new Date(`${Number(year) + 1}-01-01T00:00:00Z`).getTime() / 1000 + } + + return { + timestamp: { + gte: Math.floor(start), + lt: Math.floor(end) + } + } + }) +} export async function fetchAllPaymentsByUserId ( userId: string, networkIds?: number[], buttonIds?: string[], - years?: string[] + years?: string[], + timezone?: string ): Promise { const where: Prisma.TransactionWhereInput = { address: { @@ -878,16 +889,7 @@ export async function fetchAllPaymentsByUserId ( } if (years !== undefined && years.length > 0) { - const yearFilters = years.map((year) => { - const start = new Date(`${year}-01-01T00:00:00Z`).getTime() / 1000 - const end = new Date(`${Number(year) + 1}-01-01T00:00:00Z`).getTime() / 1000 - return { - timestamp: { - gte: Math.floor(start), - lt: Math.floor(end) - } - } - }) + const yearFilters = getYearFilters(years, timezone) where.OR = yearFilters } @@ -916,7 +918,8 @@ export async function fetchTxCountByPaybuttonId (paybuttonId: string): Promise => { const where: Prisma.TransactionWhereInput = { address: { @@ -936,16 +939,7 @@ export const getFilteredTransactionCount = async ( } } if (years !== undefined && years.length > 0) { - const yearFilters = years.map((year) => { - const start = new Date(`${year}-01-01T00:00:00Z`).getTime() / 1000 - const end = new Date(`${Number(year) + 1}-01-01T00:00:00Z`).getTime() / 1000 - return { - timestamp: { - gte: Math.floor(start), - lt: Math.floor(end) - } - } - }) + const yearFilters = getYearFilters(years, timezone) where.OR = yearFilters }