From bc24680322c5a4309e4f6d7bb8b8a4f48570dfeb Mon Sep 17 00:00:00 2001 From: lissavxo Date: Thu, 19 Mar 2026 18:38:30 -0300 Subject: [PATCH 1/2] feat: csvCollapseThreshold to user profile --- .../20260317024924_csv_collapse_threshold/migration.sql | 2 ++ prisma-local/schema.prisma | 1 + services/userService.ts | 9 +++++++++ tests/mockedObjects.ts | 6 ++++-- 4 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 prisma-local/migrations/20260317024924_csv_collapse_threshold/migration.sql diff --git a/prisma-local/migrations/20260317024924_csv_collapse_threshold/migration.sql b/prisma-local/migrations/20260317024924_csv_collapse_threshold/migration.sql new file mode 100644 index 00000000..4f44e782 --- /dev/null +++ b/prisma-local/migrations/20260317024924_csv_collapse_threshold/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE `UserProfile` ADD COLUMN `csvCollapseThreshold` INTEGER NOT NULL DEFAULT 1; diff --git a/prisma-local/schema.prisma b/prisma-local/schema.prisma index 3694300a..7a963e28 100644 --- a/prisma-local/schema.prisma +++ b/prisma-local/schema.prisma @@ -189,6 +189,7 @@ model UserProfile { invoices Invoice[] preferredCurrencyId Int @default(1) preferredTimezone String @db.VarChar(255)@default("") + csvCollapseThreshold Int @default(1) proUntil DateTime? paybuttons Paybutton[] diff --git a/services/userService.ts b/services/userService.ts index e9d6e70f..17dcc67f 100644 --- a/services/userService.ts +++ b/services/userService.ts @@ -156,6 +156,15 @@ export async function updatePreferredTimezone (id: string, preferredTimezone: st }) } +export async function updateCsvCollapseThreshold (id: string, csvCollapseThreshold: number): Promise { + await prisma.userProfile.update({ + where: { id }, + data: { + csvCollapseThreshold + } + }) +} + export async function userRemainingProTime (id: string): Promise { const today = new Date() const proUntil = (await prisma.userProfile.findUniqueOrThrow({ diff --git a/tests/mockedObjects.ts b/tests/mockedObjects.ts index 9c0b8fc2..088b9326 100644 --- a/tests/mockedObjects.ts +++ b/tests/mockedObjects.ts @@ -528,7 +528,8 @@ export const mockedUserProfile: UserProfile = { preferredTimezone: '', emailCredits: 15, postCredits: 15, - proUntil: null + proUntil: null, + csvCollapseThreshold: 0 } export const mockedUserProfileWithPublicKey: UserProfile = { @@ -543,7 +544,8 @@ export const mockedUserProfileWithPublicKey: UserProfile = { preferredTimezone: '', emailCredits: 15, postCredits: 15, - proUntil: null + proUntil: null, + csvCollapseThreshold: 0 } export const mockedAddressesOnButtons: AddressesOnButtons[] = [ From 56ef258f2518f034dc6021b0e545df3377038ffe Mon Sep 17 00:00:00 2001 From: lissavxo Date: Thu, 19 Mar 2026 18:40:02 -0300 Subject: [PATCH 2/2] feat: csv collapse threshold option --- .../Account/ChangeCsvCollapseThreshold.tsx | 90 +++++++++++++++++++ components/Account/account.module.css | 31 +++++++ pages/account/index.tsx | 10 +++ .../download/transactions/[paybuttonId].ts | 2 +- pages/api/payments/download/index.ts | 2 +- pages/api/user/csvCollapseThreshold/index.ts | 32 +++++++ 6 files changed, 165 insertions(+), 2 deletions(-) create mode 100644 components/Account/ChangeCsvCollapseThreshold.tsx create mode 100644 pages/api/user/csvCollapseThreshold/index.ts diff --git a/components/Account/ChangeCsvCollapseThreshold.tsx b/components/Account/ChangeCsvCollapseThreshold.tsx new file mode 100644 index 00000000..91fa1f1b --- /dev/null +++ b/components/Account/ChangeCsvCollapseThreshold.tsx @@ -0,0 +1,90 @@ +import React, { ReactElement, useState } from 'react' +import style from './account.module.css' +import { DEFAULT_CSV_COLLAPSE_THRESHOLD } from 'constants/index' + +interface IProps { + csvCollapseThreshold: number +} + +export default function ChangeCsvCollapseThreshold ({ csvCollapseThreshold }: IProps): ReactElement { + const [threshold, setThreshold] = useState(csvCollapseThreshold ?? DEFAULT_CSV_COLLAPSE_THRESHOLD) + const [inputValue, setInputValue] = useState(String(csvCollapseThreshold ?? DEFAULT_CSV_COLLAPSE_THRESHOLD)) + const [error, setError] = useState('') + const [success, setSuccess] = useState('') + const [disabled, setDisabled] = useState(true) + + const onSubmit = async (e: React.FormEvent): Promise => { + e.preventDefault() + const newThreshold = parseInt(inputValue, 10) + if (isNaN(newThreshold) || newThreshold < 0) { + setError('Please enter a valid non-negative number') + return + } + + const oldThreshold = threshold + setThreshold(newThreshold) + setDisabled(true) + + try { + const res = await fetch('/api/user/csvCollapseThreshold', { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ csvCollapseThreshold: newThreshold }) + }) + if (res.status === 200) { + setError('') + setSuccess('Updated successfully.') + setTimeout(() => { + setSuccess('') + }, 3000) + } else { + throw new Error('Failed to update threshold') + } + } catch (err: any) { + setSuccess('') + setError(err.message ?? 'Failed to update threshold') + setThreshold(oldThreshold) + setInputValue(String(oldThreshold)) + } + } + + const handleInputChange = (e: React.ChangeEvent): void => { + const value = e.target.value + setInputValue(value) + const numValue = parseInt(value, 10) + if (!isNaN(numValue) && numValue >= 0 && numValue !== threshold) { + setDisabled(false) + setError('') + } else if (numValue === threshold) { + setDisabled(true) + } else { + setDisabled(true) + if (value !== '') { + setError('Please enter a valid non-negative number') + } + } + } + + return ( +
+
{ void onSubmit(e) }}> +
+ + +
+ {error !== '' &&
{error}
} + {success !== '' &&
{success}
} +
+
+ ) +} diff --git a/components/Account/account.module.css b/components/Account/account.module.css index 5688b5bb..ad4c8a60 100644 --- a/components/Account/account.module.css +++ b/components/Account/account.module.css @@ -78,3 +78,34 @@ body[data-theme="dark"] .pro_ctn input { margin: 0 0 10px 0; font-weight: 600; } + +.threshold_ctn { + width: 100%; + margin-top: 10px; +} + +.threshold_row { + display: flex; + gap: 10px; + align-items: center; +} + +.threshold_input { + width: 80px; + padding: 5px 10px; + border: 1px solid #ccc; + border-radius: 5px; + font-size: 14px; +} + +body[data-theme='dark'] .threshold_input { + border-color: #a5a5a5; + background-color: var(--primary-bg-color); + color: var(--primary-text-color); +} + +.threshold_ctn button { + padding: 5px 15px; + border-radius: 5px; + font-size: 14px; +} diff --git a/pages/account/index.tsx b/pages/account/index.tsx index 2e6bde75..4b2ddc59 100644 --- a/pages/account/index.tsx +++ b/pages/account/index.tsx @@ -7,6 +7,7 @@ import { GetServerSideProps } from 'next' import Page from 'components/Page' import ChangePassword from 'components/Account/ChangePassword' import ChangeFiatCurrency from 'components/Account/ChangeFiatCurrency' +import ChangeCsvCollapseThreshold from 'components/Account/ChangeCsvCollapseThreshold' import style from './account.module.css' import { fetchUserProfileFromId, fetchUserWithSupertokens, getUserPublicKeyHex, UserWithSupertokens } from 'services/userService' import CopyIcon from '../../assets/copy-black.png' @@ -147,6 +148,15 @@ export default function Account ({ user, userPublicKey, organization, orgMembers /> +
+
CSV Collapse Threshold
+
+ +
+
+
Public Key{' '} diff --git a/pages/api/paybutton/download/transactions/[paybuttonId].ts b/pages/api/paybutton/download/transactions/[paybuttonId].ts index a7a46e13..19dd4ffc 100644 --- a/pages/api/paybutton/download/transactions/[paybuttonId].ts +++ b/pages/api/paybutton/download/transactions/[paybuttonId].ts @@ -52,7 +52,7 @@ export default async (req: any, res: any): Promise => { }; const transactions = await fetchTransactionsByPaybuttonId(paybutton.id, networkIdArray) res.setHeader('Content-Type', 'text/csv') - await downloadTxsFile(res, quoteSlug, timezone, transactions, userId, paybuttonId) + await downloadTxsFile(res, quoteSlug, timezone, transactions, userId, paybuttonId, true, user.csvCollapseThreshold) } catch (error: any) { switch (error.message) { case RESPONSE_MESSAGES.PAYBUTTON_ID_NOT_PROVIDED_400.message: diff --git a/pages/api/payments/download/index.ts b/pages/api/payments/download/index.ts index 2d3e406b..de21144b 100644 --- a/pages/api/payments/download/index.ts +++ b/pages/api/payments/download/index.ts @@ -62,7 +62,7 @@ export default async (req: any, res: any): Promise => { const transactions = await fetchAllPaymentsByUserId(userId, networkIdArray, buttonIds, years, startDate, endDate, timezone) - await downloadTxsFile(res, quoteSlug, timezone, transactions, userId) + await downloadTxsFile(res, quoteSlug, timezone, transactions, userId, undefined, true, user.csvCollapseThreshold) } catch (error: any) { switch (error.message) { case RESPONSE_MESSAGES.METHOD_NOT_ALLOWED_405.message: diff --git a/pages/api/user/csvCollapseThreshold/index.ts b/pages/api/user/csvCollapseThreshold/index.ts new file mode 100644 index 00000000..326ff17e --- /dev/null +++ b/pages/api/user/csvCollapseThreshold/index.ts @@ -0,0 +1,32 @@ +import { setSession } from 'utils/setSession' +import * as userService from 'services/userService' +import { RESPONSE_MESSAGES } from 'constants/index' + +export default async ( + req: any, + res: any +): Promise => { + await setSession(req, res, true) + + if (req.method === 'PUT') { + const session = req.session + const { csvCollapseThreshold } = req.body + + if (csvCollapseThreshold === undefined || csvCollapseThreshold === null) { + res.status(400).json({ message: 'csvCollapseThreshold is required' }) + return + } + + const threshold = Number(csvCollapseThreshold) + if (isNaN(threshold) || threshold < 0) { + res.status(400).json({ message: 'csvCollapseThreshold must be a non-negative number' }) + return + } + + await userService.updateCsvCollapseThreshold(session.userId, threshold) + res.status(200).json({ success: true }) + } else { + res.status(RESPONSE_MESSAGES.METHOD_NOT_ALLOWED_405.statusCode) + .json(RESPONSE_MESSAGES.METHOD_NOT_ALLOWED_405) + } +}