diff --git a/pages/api/dashboard/index.ts b/pages/api/dashboard/index.ts index 3603710b3..66148dc09 100644 --- a/pages/api/dashboard/index.ts +++ b/pages/api/dashboard/index.ts @@ -10,8 +10,11 @@ export default async (req: any, res: any): Promise => { const userProfile = await fetchUserProfileFromId(userId) const userPreferredTimezone = userProfile?.preferredTimezone const timezone = userPreferredTimezone !== '' ? userPreferredTimezone : userReqTimezone - - const resJSON = await CacheGet.dashboardData(userId, timezone) + let buttonIds: string[] | undefined + if (typeof req.query.buttonIds === 'string' && req.query.buttonIds !== '') { + buttonIds = (req.query.buttonIds as string).split(',') + } + const resJSON = await CacheGet.dashboardData(userId, timezone, buttonIds) res.status(200).json(resJSON) } } diff --git a/pages/api/payments/count/index.ts b/pages/api/payments/count/index.ts index eb158d821..51b9a3fe4 100644 --- a/pages/api/payments/count/index.ts +++ b/pages/api/payments/count/index.ts @@ -21,8 +21,8 @@ export default async (req: any, res: any): Promise => { const totalCount = await getFilteredTransactionCount(userId, buttonIds) res.status(200).json(totalCount) } else { - const resJSON = await CacheGet.paymentsCount(userId, timezone) - res.status(200).json(resJSON) + const totalCount = await CacheGet.paymentsCount(userId, timezone) + res.status(200).json(totalCount) } } else { res.status(405).json({ error: 'Method not allowed' }) diff --git a/pages/dashboard/dashboard.module.css b/pages/dashboard/dashboard.module.css index bbcf0bb8f..ce0d84167 100644 --- a/pages/dashboard/dashboard.module.css +++ b/pages/dashboard/dashboard.module.css @@ -160,3 +160,93 @@ font-size: 16px; } } + +.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; +} \ No newline at end of file diff --git a/pages/dashboard/index.tsx b/pages/dashboard/index.tsx index 91c866aca..edce4804e 100644 --- a/pages/dashboard/index.tsx +++ b/pages/dashboard/index.tsx @@ -13,6 +13,9 @@ import { loadStateFromCookie, saveStateToCookie } from 'utils/cookies' import TopBar from 'components/TopBar' import { fetchUserWithSupertokens, UserWithSupertokens } from 'services/userService' import moment from 'moment-timezone' +import SettingsIcon from '../../assets/settings-slider-icon.png' +import Image from 'next/image' + const Chart = dynamic(async () => await import('components/Chart'), { ssr: false }) @@ -70,6 +73,9 @@ export default function Dashboard ({ user }: PaybuttonsProps): React.ReactElemen const [activePeriod, setActivePeriod] = useState() const [activePeriodString, setActivePeriodString] = useState('1M') const [totalString, setTotalString] = useState() + const [selectedButtonIds, setSelectedButtonIds] = useState([]) + const [showFilters, setShowFilters] = useState(false) + const [buttons, setButtons] = useState([]) const setPeriodFromString = (data?: DashboardData, periodString?: PeriodString): void => { if (data === undefined) return @@ -92,10 +98,30 @@ export default function Dashboard ({ user }: PaybuttonsProps): React.ReactElemen } saveStateToCookie(COOKIE_NAMES.DASHBOARD_FILTER, periodString) } + const getDataAndSetUpButtons = async (): Promise => { + const paybuttons = await fetchPaybuttons() + setButtons(paybuttons) + } + const fetchPaybuttons = async (): Promise => { + const res = await fetch(`/api/paybuttons?userId=${user?.userProfile.id}`, { + method: 'GET' + }) + if (res.status === 200) { + return await res.json() + } + } + + useEffect(() => { + void getDataAndSetUpButtons() + }, []) useEffect(() => { const fetchData = async (): Promise => { - const res = await fetch('api/dashboard', { + let url = 'api/dashboard' + if (selectedButtonIds.length > 0) { + url += `?buttonIds=${selectedButtonIds.join(',')}` + } + const res = await fetch(url, { headers: { Timezone: moment.tz.guess() } @@ -108,7 +134,7 @@ export default function Dashboard ({ user }: PaybuttonsProps): React.ReactElemen if (savedActivePeriodString !== undefined) { setActivePeriodString(savedActivePeriodString) } - }, []) + }, [selectedButtonIds]) useEffect(() => { setPeriodFromString(dashboardData, activePeriodString) @@ -127,6 +153,44 @@ export default function Dashboard ({ user }: PaybuttonsProps): React.ReactElemen return ( <> +
+
setShowFilters(!showFilters)} + className={style.show_filters_button} + > + filtersFilters +
+ {selectedButtonIds.length > 0 && +
setSelectedButtonIds([])} + className={style.show_filters_button} + > + Clear +
+ } +
+ {showFilters && ( +
+ Filter by PayButton +
+ {buttons.map((button) => ( +
{ + 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} +
+ ))} +
+
+ )}
diff --git a/redis/dashboardCache.ts b/redis/dashboardCache.ts index b4db64840..b1e2ee5c4 100644 --- a/redis/dashboardCache.ts +++ b/redis/dashboardCache.ts @@ -110,7 +110,8 @@ const generateDashboardDataFromStream = async function ( paymentStream: AsyncGenerator, nMonthsTotal: number, borderColor: ChartColor, - timezone: string + timezone: string, + paybuttonIds?: string[] ): Promise { const revenueAccumulators = createRevenueAccumulators(nMonthsTotal) const paymentCounters = createPaymentCounters(nMonthsTotal) @@ -133,7 +134,13 @@ const generateDashboardDataFromStream = async function ( // Process button data and assign to relevant periods payment.buttonDisplayDataList.forEach((button) => { - processButtonData(button, payment, paymentTime, buttonDataAccumulators, thresholds) + if (paybuttonIds !== undefined && paybuttonIds.length > 0) { + if (paybuttonIds.includes(button.id)) { + processButtonData(button, payment, paymentTime, buttonDataAccumulators, thresholds) + } + } else { + processButtonData(button, payment, paymentTime, buttonDataAccumulators, thresholds) + } }) // Accumulate period data @@ -151,8 +158,16 @@ const generateDashboardDataFromStream = async function ( } if (index >= 0 && index < revenueAccumulators[period].length) { - revenueAccumulators[period][index] = sumQuoteValues(revenueAccumulators[period][index], payment.values.values) - paymentCounters[period][index] += 1 + if (paybuttonIds !== undefined && paybuttonIds.length > 0) { + const paymentButtonIds = payment.buttonDisplayDataList.map(b => b.id) + if (paymentButtonIds.some(item => paybuttonIds.includes(item))) { + revenueAccumulators[period][index] = sumQuoteValues(revenueAccumulators[period][index], payment.values.values) + paymentCounters[period][index] += 1 + } + } else { + revenueAccumulators[period][index] = sumQuoteValues(revenueAccumulators[period][index], payment.values.values) + paymentCounters[period][index] += 1 + } } } } @@ -212,7 +227,8 @@ const generateDashboardDataFromStream = async function ( revenue: all.totalRevenue, payments: all.totalPayments, buttons: Object.keys(buttonDataAccumulators.all).length - } + }, + filtered: paybuttonIds !== undefined && paybuttonIds.length > 0 } } @@ -345,8 +361,12 @@ function createPeriodData ( } } -export const getUserDashboardData = async function (userId: string, timezone: string): Promise { - const dashboardData = await getCachedDashboardData(userId) +export const getUserDashboardData = async function (userId: string, timezone: string, paybuttonIds?: string[]): Promise { + let dashboardData = await getCachedDashboardData(userId) + if ((paybuttonIds !== undefined && paybuttonIds.length > 0) || + dashboardData?.filtered === true) { + dashboardData = null + } if (dashboardData === null) { console.log('[CACHE]: Recreating dashboard for user', userId) const nMonthsTotal = await getNumberOfMonths(userId) @@ -356,7 +376,8 @@ export const getUserDashboardData = async function (userId: string, timezone: st paymentStream, nMonthsTotal, { revenue: '#66fe91', payments: '#669cfe' }, - timezone + timezone, + paybuttonIds ) await cacheDashboardData(userId, dashboardData) return dashboardData diff --git a/redis/index.ts b/redis/index.ts index cc524385f..3fc5f35d4 100644 --- a/redis/index.ts +++ b/redis/index.ts @@ -108,9 +108,9 @@ export class CacheGet { } } - static async dashboardData (userId: string, timezone: string): Promise { + static async dashboardData (userId: string, timezone: string, buttonIds?: string[]): Promise { return await this.executeCall(userId, 'dashboardData', async () => { - return await getUserDashboardData(userId, timezone) + return await getUserDashboardData(userId, timezone, buttonIds) }) } diff --git a/redis/types.ts b/redis/types.ts index a5dcd782e..4abbfb1b4 100644 --- a/redis/types.ts +++ b/redis/types.ts @@ -35,6 +35,7 @@ export interface DashboardData { payments: number buttons: number } + filtered: boolean } export interface ButtonDisplayData { diff --git a/tests/integration-tests/api.test.ts b/tests/integration-tests/api.test.ts index 6ca945152..d06d0b68e 100644 --- a/tests/integration-tests/api.test.ts +++ b/tests/integration-tests/api.test.ts @@ -1369,7 +1369,8 @@ describe('GET /api/dashboard', () => { }, payments: expect.any(Number), buttons: expect.any(Number) - } + }, + filtered: expect.any(Boolean) } ) })