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/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/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/api/payments/index.ts b/pages/api/payments/index.ts index d626f9732..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 => { @@ -14,6 +15,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(',') } + let years: string[] | undefined + 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, @@ -21,7 +29,9 @@ export default async (req: any, res: any): Promise => { pageSize, orderBy, orderDesc, - buttonIds + buttonIds, + years, + userPreferredTimezone ?? userReqTimezone ) res.status(200).json(resJSON) } diff --git a/pages/api/transaction/years/index.ts b/pages/api/transaction/years/index.ts new file mode 100644 index 000000000..2ea7782e4 --- /dev/null +++ b/pages/api/transaction/years/index.ts @@ -0,0 +1,15 @@ +import { fetchDistinctPaymentYearsByUser } 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 fetchDistinctPaymentYearsByUser(userId) + res.status(200).json({ years }) + } catch (err: any) { + res.status(500).json({ statusCode: 500, message: err.message }) + } + } +} diff --git a/pages/payments/index.tsx b/pages/payments/index.tsx index 21158dff5..e85d54184 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,24 @@ 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) + const paymentsResponse = await fetch(url, { + headers: { + Timezone: moment.tz.guess() + } + }) + 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 } } ) @@ -309,7 +340,7 @@ export default function Payments ({ user, userId, organization }: PaybuttonsProp create invoice -
New button
+
New Invoice
) : ( @@ -353,6 +384,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)) { @@ -403,7 +437,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 +452,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 +493,7 @@ export default function Payments ({ user, userId, organization }: PaybuttonsProp )}
{showFilters && ( +
Filter by button
@@ -476,6 +514,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} +
+ ))} +
+
+
)} { const orderDescString: Prisma.SortOrder = orderDesc ? 'desc' : 'asc' @@ -798,6 +796,11 @@ export async function fetchAllPaymentsByUserIdWithPagination ( gt: 0 } } + if (years !== undefined && years.length > 0) { + const yearFilters = getYearFilters(years, timezone) + + where.OR = yearFilters + } if ((buttonIds !== undefined) && buttonIds.length > 0) { where.address!.paybuttons = { @@ -827,11 +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[] + buttonIds?: string[], + years?: string[], + timezone?: string ): Promise { const where: Prisma.TransactionWhereInput = { address: { @@ -857,6 +888,12 @@ export async function fetchAllPaymentsByUserId ( } } + if (years !== undefined && years.length > 0) { + const yearFilters = getYearFilters(years, timezone) + + where.OR = yearFilters + } + return await prisma.transaction.findMany({ where, include: includePaybuttonsAndPrices, @@ -880,21 +917,46 @@ 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 = getYearFilters(years, timezone) + + where.OR = yearFilters + } + + return await prisma.transaction.count({ where }) +} + +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} AND + t.amount > 0 + ORDER BY year ASC + ` + + return years.map(y => y.year) }