From 62221312fd4064fbd37d7954f92c547167aabc95 Mon Sep 17 00:00:00 2001 From: zachery with an e <45150570+zweatshirt@users.noreply.github.com> Date: Wed, 20 May 2026 10:39:26 -0500 Subject: [PATCH 1/3] Add getStaffExpenseMonthRange function and tests for month range calculation --- .../Helpers/getMonthRange.test.ts | 62 +++++++++++++++++++ .../Helpers/getMonthRange.ts | 19 ++++++ .../SettingsDialog/SettingsDialog.tsx | 9 +-- .../StaffExpenseReport/StaffExpenseReport.tsx | 9 +-- 4 files changed, 85 insertions(+), 14 deletions(-) create mode 100644 src/components/Reports/StaffExpenseReport/Helpers/getMonthRange.test.ts create mode 100644 src/components/Reports/StaffExpenseReport/Helpers/getMonthRange.ts diff --git a/src/components/Reports/StaffExpenseReport/Helpers/getMonthRange.test.ts b/src/components/Reports/StaffExpenseReport/Helpers/getMonthRange.test.ts new file mode 100644 index 0000000000..8f812a3ce8 --- /dev/null +++ b/src/components/Reports/StaffExpenseReport/Helpers/getMonthRange.test.ts @@ -0,0 +1,62 @@ +import { DateTime } from 'luxon'; +import { getStaffExpenseMonthRange } from './getMonthRange'; + +const baseTime = DateTime.fromISO('2020-01-15'); + +describe('getStaffExpenseMonthRange', () => { + it('falls back to baseTime month when filters is null', () => { + expect(getStaffExpenseMonthRange(null, baseTime)).toEqual({ + startMonth: '2020-01-01', + endMonth: '2020-01-31', + }); + }); + + it('falls back to baseTime month when filters is undefined', () => { + expect(getStaffExpenseMonthRange(undefined, baseTime)).toEqual({ + startMonth: '2020-01-01', + endMonth: '2020-01-31', + }); + }); + + it('falls back to baseTime month when both dates are null', () => { + expect( + getStaffExpenseMonthRange({ startDate: null, endDate: null }, baseTime), + ).toEqual({ + startMonth: '2020-01-01', + endMonth: '2020-01-31', + }); + }); + + it('uses startDate and endDate months when both are present', () => { + const filters = { + startDate: DateTime.fromISO('2025-03-10'), + endDate: DateTime.fromISO('2025-05-20'), + }; + expect(getStaffExpenseMonthRange(filters, baseTime)).toEqual({ + startMonth: '2025-03-01', + endMonth: '2025-05-31', + }); + }); + + it('uses startDate month and baseTime endMonth when only startDate is present', () => { + const filters = { + startDate: DateTime.fromISO('2025-03-10'), + endDate: null, + }; + expect(getStaffExpenseMonthRange(filters, baseTime)).toEqual({ + startMonth: '2025-03-01', + endMonth: '2020-01-31', + }); + }); + + it('uses endDate month for both startMonth and endMonth when only endDate is present', () => { + const filters = { + startDate: null, + endDate: DateTime.fromISO('2025-05-20'), + }; + expect(getStaffExpenseMonthRange(filters, baseTime)).toEqual({ + startMonth: '2025-05-01', + endMonth: '2025-05-31', + }); + }); +}); diff --git a/src/components/Reports/StaffExpenseReport/Helpers/getMonthRange.ts b/src/components/Reports/StaffExpenseReport/Helpers/getMonthRange.ts new file mode 100644 index 0000000000..e13cd10d82 --- /dev/null +++ b/src/components/Reports/StaffExpenseReport/Helpers/getMonthRange.ts @@ -0,0 +1,19 @@ +import { DateTime } from 'luxon'; + +interface DateWindow { + startDate?: DateTime | null; + endDate?: DateTime | null; +} + +export const getStaffExpenseMonthRange = ( + filters: DateWindow | null | undefined, + baseTime: DateTime, +): { startMonth: string | null; endMonth: string | null } => ({ + startMonth: + filters?.startDate?.startOf('month').toISODate() ?? + filters?.endDate?.startOf('month').toISODate() ?? + baseTime.startOf('month').toISODate(), + endMonth: + filters?.endDate?.endOf('month').toISODate() ?? + baseTime.endOf('month').toISODate(), +}); diff --git a/src/components/Reports/StaffExpenseReport/SettingsDialog/SettingsDialog.tsx b/src/components/Reports/StaffExpenseReport/SettingsDialog/SettingsDialog.tsx index 8f713a5d8f..38c68111a7 100644 --- a/src/components/Reports/StaffExpenseReport/SettingsDialog/SettingsDialog.tsx +++ b/src/components/Reports/StaffExpenseReport/SettingsDialog/SettingsDialog.tsx @@ -26,6 +26,7 @@ import { getLocalizedCategory } from '../../Shared/Helpers/transformStaffExpense import { useReportsStaffExpensesQuery } from '../GetStaffExpense.generated'; import { DateRange } from '../Helpers/StaffReportEnum'; import { getAvailableCategories } from '../Helpers/filterTransactions'; +import { getStaffExpenseMonthRange } from '../Helpers/getMonthRange'; export interface SettingsDialogProps { isOpen: boolean; @@ -133,13 +134,7 @@ export const SettingsDialog: React.FC = ({ const getQueryVariables = (filterParams: Filters | null) => ({ fundTypes: selectedFundType ? [selectedFundType] : null, - startMonth: - filterParams?.startDate?.startOf('month').toISODate() ?? - filterParams?.endDate?.startOf('month').toISODate() ?? - currentTime.startOf('month').toISODate(), - endMonth: - filterParams?.endDate?.endOf('month').toISODate() ?? - currentTime.endOf('month').toISODate(), + ...getStaffExpenseMonthRange(filterParams, currentTime), }); const { diff --git a/src/components/Reports/StaffExpenseReport/StaffExpenseReport.tsx b/src/components/Reports/StaffExpenseReport/StaffExpenseReport.tsx index 20cc1582d4..c492691ff4 100644 --- a/src/components/Reports/StaffExpenseReport/StaffExpenseReport.tsx +++ b/src/components/Reports/StaffExpenseReport/StaffExpenseReport.tsx @@ -45,6 +45,7 @@ import { getIconColorForFundType, getIconForFundType, } from './Helpers/fundTypeHelpers'; +import { getStaffExpenseMonthRange } from './Helpers/getMonthRange'; import { Filters, SettingsDialog } from './SettingsDialog/SettingsDialog'; import { PrintTables } from './Tables/PrintTables'; import { StaffReportTable } from './Tables/StaffReportTable'; @@ -98,13 +99,7 @@ export const StaffExpenseReport: React.FC = ({ const { data, loading } = useReportsStaffExpensesQuery({ variables: { fundTypes: ['Primary', 'Savings', 'Staff Conference Savings'], - startMonth: - filters?.startDate?.startOf('month').toISODate() ?? - filters?.endDate?.startOf('month').toISODate() ?? - time.startOf('month').toISODate(), - endMonth: - filters?.endDate?.endOf('month').toISODate() ?? - time.endOf('month').toISODate(), + ...getStaffExpenseMonthRange(filters, time), }, }); From 35bb5e16d24f8ff53521ab162cbe1dc5c95e1283 Mon Sep 17 00:00:00 2001 From: zachery with an e <45150570+zweatshirt@users.noreply.github.com> Date: Wed, 20 May 2026 10:40:46 -0500 Subject: [PATCH 2/3] Replace Loading component with CircularProgress for better loading indication in SettingsDialog --- .../StaffExpenseReport/SettingsDialog/SettingsDialog.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/Reports/StaffExpenseReport/SettingsDialog/SettingsDialog.tsx b/src/components/Reports/StaffExpenseReport/SettingsDialog/SettingsDialog.tsx index 38c68111a7..aa81debe34 100644 --- a/src/components/Reports/StaffExpenseReport/SettingsDialog/SettingsDialog.tsx +++ b/src/components/Reports/StaffExpenseReport/SettingsDialog/SettingsDialog.tsx @@ -4,6 +4,7 @@ import { Box, Button, Checkbox, + CircularProgress, Dialog, DialogActions, DialogContent, @@ -18,7 +19,6 @@ import { Form, Formik } from 'formik'; import { DateTime } from 'luxon'; import { useTranslation } from 'react-i18next'; import * as yup from 'yup'; -import Loading from 'src/components/Loading/Loading'; import { CustomDateField } from 'src/components/Shared/DateTimePickers/CustomDateField'; import { Fund, StaffExpenseCategoryEnum } from 'src/graphql/types.generated'; import i18n from 'src/lib/i18n'; @@ -313,7 +313,11 @@ export const SettingsDialog: React.FC = ({ {categoryLoading ? ( - + + + ) : categoryError ? ( {t('Failed to load categories. Please try again.')} From 20657d9f879347b15cd1c93b95804adf44cce9d2 Mon Sep 17 00:00:00 2001 From: zachery with an e <45150570+zweatshirt@users.noreply.github.com> Date: Wed, 20 May 2026 10:42:33 -0500 Subject: [PATCH 3/3] Add time prop to SettingsDialog and StaffExpenseReport for improved date handling in reports --- .../SettingsDialog/SettingsDialog.test.tsx | 45 +++++++++++--- .../SettingsDialog/SettingsDialog.tsx | 9 ++- .../StaffExpenseReport.test.tsx | 59 +++++++++++++++++++ .../StaffExpenseReport/StaffExpenseReport.tsx | 3 +- 4 files changed, 107 insertions(+), 9 deletions(-) diff --git a/src/components/Reports/StaffExpenseReport/SettingsDialog/SettingsDialog.test.tsx b/src/components/Reports/StaffExpenseReport/SettingsDialog/SettingsDialog.test.tsx index 4e7d5451bc..46e8e84b9f 100644 --- a/src/components/Reports/StaffExpenseReport/SettingsDialog/SettingsDialog.test.tsx +++ b/src/components/Reports/StaffExpenseReport/SettingsDialog/SettingsDialog.test.tsx @@ -11,14 +11,20 @@ import { ReportsStaffExpensesQuery } from '../GetStaffExpense.generated'; import { DateRange } from '../Helpers/StaffReportEnum'; import { Filters, SettingsDialog, SettingsDialogProps } from './SettingsDialog'; -const TestComponent: React.FC = ({ +const mutationSpy = jest.fn(); +const TestComponent: React.FC< + SettingsDialogProps & { onCallMock?: jest.Mock } +> = ({ isOpen, onClose, selectedFilters, selectedFundType, + time, + onCallMock, }) => ( + onCall={onCallMock} mocks={{ ReportsStaffExpenses: { reportsStaffExpenses: { @@ -46,9 +52,7 @@ const TestComponent: React.FC = ({ breakdownByMonth: [ { transactions: [ - { - transactedAt: DateTime.now().toISO(), - }, + { transactedAt: DateTime.now().toISO() }, ], }, ], @@ -62,9 +66,7 @@ const TestComponent: React.FC = ({ breakdownByMonth: [ { transactions: [ - { - transactedAt: DateTime.now().toISO(), - }, + { transactedAt: DateTime.now().toISO() }, ], }, ], @@ -84,6 +86,7 @@ const TestComponent: React.FC = ({ onClose={onClose} selectedFilters={selectedFilters} selectedFundType={selectedFundType} + time={time} /> @@ -418,4 +421,32 @@ describe('SettingsDialog', () => { expect(await findByLabelText('Benefits')).not.toBeChecked(); }); + + it('should query using the time prop month when no date filter is set', async () => { + const time = DateTime.fromISO('2025-10-01'); + + render( + , + ); + + await waitFor(() => { + expect(mutationSpy).toHaveGraphqlOperation('ReportsStaffExpenses', { + startMonth: '2025-10-01', + endMonth: '2025-10-31', + fundTypes: ['Primary'], + }); + }); + }); + + it('should fall back to current month for category query when time prop is not provided', async () => { + render(); + + await waitFor(() => { + expect(mutationSpy).toHaveGraphqlOperation('ReportsStaffExpenses', { + startMonth: '2020-01-01', + endMonth: '2020-01-31', + fundTypes: ['Primary'], + }); + }); + }); }); diff --git a/src/components/Reports/StaffExpenseReport/SettingsDialog/SettingsDialog.tsx b/src/components/Reports/StaffExpenseReport/SettingsDialog/SettingsDialog.tsx index aa81debe34..b5d3507521 100644 --- a/src/components/Reports/StaffExpenseReport/SettingsDialog/SettingsDialog.tsx +++ b/src/components/Reports/StaffExpenseReport/SettingsDialog/SettingsDialog.tsx @@ -33,6 +33,7 @@ export interface SettingsDialogProps { selectedFilters?: Filters; selectedFundType: string | null; onClose: (filters?: Filters) => void; + time?: DateTime; } export interface Filters { @@ -121,11 +122,15 @@ export const SettingsDialog: React.FC = ({ onClose, selectedFilters, selectedFundType, + time, }) => { const { t } = useTranslation(); const [previewFilters, setPreviewFilters] = useState(null); - const currentTime = useMemo(() => DateTime.now().startOf('month'), []); + const currentTime = useMemo( + () => time ?? DateTime.now().startOf('month'), + [time], + ); const handleClose = () => { setPreviewFilters(null); @@ -143,6 +148,8 @@ export const SettingsDialog: React.FC = ({ error: categoryError, } = useReportsStaffExpensesQuery({ variables: getQueryVariables(previewFilters ?? selectedFilters ?? null), + skip: !isOpen, + fetchPolicy: 'no-cache', }); const availableCategories = useMemo(() => { diff --git a/src/components/Reports/StaffExpenseReport/StaffExpenseReport.test.tsx b/src/components/Reports/StaffExpenseReport/StaffExpenseReport.test.tsx index aa71ce3d7e..e449aebb04 100644 --- a/src/components/Reports/StaffExpenseReport/StaffExpenseReport.test.tsx +++ b/src/components/Reports/StaffExpenseReport/StaffExpenseReport.test.tsx @@ -4,6 +4,7 @@ import { LocalizationProvider } from '@mui/x-date-pickers'; import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon'; import { render, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { Settings } from 'luxon'; import { SnackbarProvider } from 'notistack'; import TestRouter from '__tests__/util/TestRouter'; import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; @@ -274,4 +275,62 @@ describe('StaffExpenseReport', () => { expect(getAllByRole('row')).toHaveLength(2); }); }); + + it('shows month title and navigation when only category filters are applied', async () => { + const { getByRole, findByRole, findByLabelText, queryByText } = render( + , + ); + + userEvent.click(getByRole('button', { name: 'Filter Settings' })); + + userEvent.click(await findByLabelText('Assessment')); + userEvent.click(await findByRole('button', { name: 'Apply Filters' })); + + expect( + await findByRole('heading', { name: 'January 2020', level: 6 }), + ).toBeInTheDocument(); + expect( + await findByRole('button', { name: 'Previous Month' }), + ).toBeInTheDocument(); + expect( + await findByRole('button', { name: 'Next Month' }), + ).toBeInTheDocument(); + expect(queryByText('Clear Filters')).not.toBeInTheDocument(); + }); + + it('shows filter date range title and hides month navigation when date filters are applied', async () => { + const originalNow = Settings.now; + Settings.now = () => new Date(2020, 0, 20).valueOf(); + + const { getByRole, findByRole, getByLabelText, queryByRole } = render( + , + ); + + userEvent.click(getByRole('button', { name: 'Filter Settings' })); + await findByRole('heading', { name: 'Report Settings' }); + + userEvent.click(getByLabelText('Select Date Range')); + userEvent.click(getByRole('option', { name: 'Month to Date' })); + + await waitFor(() => + expect(getByRole('button', { name: 'Apply Filters' })).not.toBeDisabled(), + ); + userEvent.click(getByRole('button', { name: 'Apply Filters' })); + + expect( + await findByRole('heading', { + level: 6, + name: 'January 1, 2020 - January 20, 2020', + }), + ).toBeInTheDocument(); + + expect( + queryByRole('button', { name: 'Previous Month' }), + ).not.toBeInTheDocument(); + expect( + queryByRole('button', { name: 'Next Month' }), + ).not.toBeInTheDocument(); + + Settings.now = originalNow; + }, 10000); }); diff --git a/src/components/Reports/StaffExpenseReport/StaffExpenseReport.tsx b/src/components/Reports/StaffExpenseReport/StaffExpenseReport.tsx index c492691ff4..bfdfec386d 100644 --- a/src/components/Reports/StaffExpenseReport/StaffExpenseReport.tsx +++ b/src/components/Reports/StaffExpenseReport/StaffExpenseReport.tsx @@ -337,7 +337,7 @@ export const StaffExpenseReport: React.FC = ({ - {!filters ? ( + {!isFilterDateSelected ? ( {timeTitle} ) : ( {filterTimeTitle} @@ -419,6 +419,7 @@ export const StaffExpenseReport: React.FC = ({ selectedFilters={filters || undefined} selectedFundType={selectedFundType} isOpen={isSettingsOpen} + time={time} onClose={(newFilters) => { setFilters(newFilters ?? null); setIsSettingsOpen(false);