Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,22 @@ import MPGAReportPage, { getServerSideProps } from './index.page';

const mutationSpy = jest.fn();
const id = '1000000001';
const personNumber = '000000111';

interface ComponentProps {
userType?: UserTypeEnum;
staffAccountId?: string | null;
supervisesStaff?: boolean;
viewedStaffAccountId?: string;
viewedPersonNumber?: string;
}

const Components = ({
userType = UserTypeEnum.UsStaff,
staffAccountId = '12345',
supervisesStaff = false,
viewedStaffAccountId,
viewedPersonNumber,
}: ComponentProps) => (
<ThemeProvider theme={theme}>
<TestRouter
Expand All @@ -38,6 +41,7 @@ const Components = ({
...(viewedStaffAccountId && {
staffAccountId: viewedStaffAccountId,
}),
...(viewedPersonNumber && { personNumber: viewedPersonNumber }),
},
}}
>
Expand Down Expand Up @@ -158,5 +162,19 @@ describe('MPGA Report Page', () => {
}),
);
});

it('passes the person number from the url into the household query', async () => {
render(
<Components
supervisesStaff
viewedStaffAccountId={id}
viewedPersonNumber={personNumber}
/>,
);

await waitFor(() =>
expect(mutationSpy).toHaveGraphqlOperation('Hcm', { personNumber }),
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ const MPGAReportPage: React.FC = () => {
const { t } = useTranslation();
const { query } = useRouter();
// A blank param is a request for somebody else, which the API denies.
// Only a missing one falls back to your own account.
// Only a missing one falls back to your own account and HCM record.
const staffAccountId = getQueryParam(query, 'staffAccountId') || undefined;
const personNumber = getQueryParam(query, 'personNumber') || undefined;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

personNumber is read from the URL independently of staffAccountId; a supervisor URL carrying only staffAccountId (bookmark, hand-built link, any producer other than ViewReportLink) skips Hcm and silently renders one combined Salary row for a couple, and a personNumber of a different supervised employee is never cross-checked against the account.

Failure scenario: Supervisor bookmarked ?staffAccountId=1000000001 before this PR -> skip: isSupervisorView && !personNumber -> combined 'Salary' row while the staff member's own view shows two rows; nothing hints the split exists. With ?staffAccountId=AAA&personNumber=BBB where BBB is another supervised employee, Hcm returns BBB's household, so AAA's real people render as 'Salary (Spouse)' and unattributed payroll is credited to BBB. Same pattern already exists on the staffExpense page, so this is a shared design gap rather than a regression.


const [isNavListOpen, setIsNavListOpen] = useState<boolean>(false);

Expand Down Expand Up @@ -64,7 +65,10 @@ const MPGAReportPage: React.FC = () => {
leftOpen={isNavListOpen}
leftWidth="290px"
mainContent={
<MPGAIncomeExpensesReportProvider staffAccountId={staffAccountId}>
<MPGAIncomeExpensesReportProvider
staffAccountId={staffAccountId}
personNumber={personNumber}
>
<MPGAIncomeExpensesReport
isNavListOpen={isNavListOpen}
onNavListToggle={handleNavListToggle}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,29 @@ import theme from 'src/theme';
import { StaffTabMPGA } from './MPGA';

const staffAccountId = '1000000001';
const personNumber = '000000111';
const accountListId = 'account-list-1';
const router = { query: { accountListId }, isReady: true };

const renderMPGA = (staffAccountId: string) =>
render(
<ThemeProvider theme={theme}>
<TestRouter router={router}>
<StaffTabMPGA staffAccountId={staffAccountId} />
<StaffTabMPGA
staffAccountId={staffAccountId}
personNumber={personNumber}
/>
</TestRouter>
</ThemeProvider>,
);

describe('StaffTabMPGA', () => {
it('links to the staff member MPGA report', () => {
it('links to the staff member MPGA report with their person number', () => {
const { getByRole } = renderMPGA(staffAccountId);

expect(getByRole('link', { name: 'View MPGA Report' })).toHaveAttribute(
'href',
`/accountLists/${accountListId}/reports/mpgaIncomeExpenses?staffAccountId=1000000001`,
`/accountLists/${accountListId}/reports/mpgaIncomeExpenses?staffAccountId=${staffAccountId}&personNumber=${personNumber}`,
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import { ViewReportLink } from '../ViewReportLink/ViewReportLink';

interface StaffTabMPGAProps {
staffAccountId: string;
personNumber: string;
}

export const StaffTabMPGA: React.FC<StaffTabMPGAProps> = ({
staffAccountId,
personNumber,
}) => {
const { t } = useTranslation();

Expand All @@ -16,6 +18,7 @@ export const StaffTabMPGA: React.FC<StaffTabMPGAProps> = ({
staffAccountId={staffAccountId}
reportLink="mpgaIncomeExpenses"
reportName={t('MPGA')}
personNumber={personNumber}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,10 @@
<DynamicPayroll staffAccountId={staffAccountId} />
</TabPanel>
<TabPanel value={StaffDetailTabEnum.MPGAReport}>
<DynamicMPGA staffAccountId={staffAccountId} />
<DynamicMPGA
staffAccountId={staffAccountId}
personNumber={personNumber}
/>

Check warning on line 266 in src/components/HrTools/MpdSupervisorReport/StaffMemberDrawer/StaffMemberDrawer.tsx

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ Getting worse: Large Method

StaffMemberDrawer:React.FC increases from 190 to 193 lines of code, threshold = 120 Large functions with many lines of code are generally harder to understand and lower the code health. Avoid adding more lines to this function.
</TabPanel>
<TabPanel value={StaffDetailTabEnum.StaffExpenseReport}>
<StaffTabStaffExpenseReport
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,27 @@ describe('BreakdownModal', () => {
expect(getByRole('dialog')).toBeInTheDocument();
expect(queryByText('Donation - Non Cash')).not.toBeInTheDocument();
});

it('names the person whose row is being broken down', () => {
const { getByText } = render(
<TestComponent
{...defaultProps}
category={StaffExpenseCategoryEnum.Salary}
person="Alex"
/>,
);

expect(getByText('Salary (Alex) Breakdown')).toBeInTheDocument();
expect(getByText('Total Salary (Alex) Income')).toBeInTheDocument();
});

it('ignores a person on any category other than salary', () => {
const { getByText, queryByText } = render(
<TestComponent {...defaultProps} person="Alex" />,
);

expect(getByText('Donation Breakdown')).toBeInTheDocument();
expect(getByText('Total Donation Income')).toBeInTheDocument();
expect(queryByText(/Alex/)).not.toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
Typography,
} from '@mui/material';
import { useTranslation } from 'react-i18next';
import { StaffExpensesSubCategoryEnum } from 'src/graphql/types.generated';
import {
StaffExpenseCategoryEnum,
StaffExpensesSubCategoryEnum,
} from 'src/graphql/types.generated';
import { useLocale } from 'src/hooks/useLocale';
import { currencyFormat, monthYearFormat } from 'src/lib/intlFormat';
import theme from 'src/theme';
Expand All @@ -29,125 +32,130 @@
open,
onClose,
category,
person,
transactions,
}) => {
const { t } = useTranslation();
const locale = useLocale();
const currency = 'USD';
const { startDate, endDate } = useMPGAIncomeExpenses();

// Salary is the only category split one row per person. Its breakdown is named the way the row
// is, so two people's modals cannot be mistaken for each other; no other category names anyone.
const categoryName =
person && category === StaffExpenseCategoryEnum.Salary
? t('{{bucket}} ({{person}})', {
bucket: getLocalizedCategory(category, t),
person,
})
: getLocalizedCategory(category, t);

const subcategoryBreakdown = useMemo(() => {
const grouped = new Map<
StaffExpensesSubCategoryEnum,
TransactionBreakdown[]
>();

transactions.forEach((transaction) => {
const existing = grouped.get(transaction.subCategory);
if (existing) {
existing.push(transaction);
} else {
grouped.set(transaction.subCategory, [transaction]);
}
});

return Array.from(grouped, ([subCategory, subCategoryTransactions]) => ({
category,
subCategory,
transactions: subCategoryTransactions,
total: subCategoryTransactions.reduce(
(sum, { amount }) => sum + amount,
0,
),
}));
}, [transactions, category]);

const overallTotal = useMemo(
() => subcategoryBreakdown.reduce((sum, { total }) => sum + total, 0),
[subcategoryBreakdown],
);

return (
<DialogSkeleton
categoryName={getLocalizedCategory(category, t)}
open={open}
onClose={onClose}
>
<DialogSkeleton categoryName={categoryName} open={open} onClose={onClose}>
<TableContainer
sx={{
borderBottom: `1px solid ${theme.palette.divider}`,
}}
>
<Table>
<TableHead>
<TableRow
sx={{
backgroundColor: theme.palette.mpdxGrayLight.main,
position: 'sticky',
top: 0,
zIndex: 1,
}}
>
<TableCell>{t('Category')}</TableCell>
<TableCell sx={{ textAlign: 'right' }}>
{`${monthYearFormat(
startDate.month,
startDate.year,
locale,
true,
true,
)} - ${monthYearFormat(
endDate.month,
endDate.year,
locale,
true,
true,
)}`}
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{subcategoryBreakdown.map(
({
subCategory,
transactions: subCategoryTransactions,
total,
}) => (
<TableRow key={subCategory}>
<TableCell colSpan={2} sx={{ padding: 0, border: 0 }}>
<BreakdownAccordion
category={category}
subCategory={subCategory}
transactions={subCategoryTransactions}
total={total}
/>
</TableCell>
</TableRow>
),
)}
</TableBody>
<TableFooter
sx={{
'& .MuiTableCell-footer': {
position: 'sticky',
bottom: 0,
backgroundColor: 'background.paper',
borderBottom: 0,
},
}}
>
<TableRow>
<TableCell>
<Typography
color={theme.palette.text.primary}
fontWeight="bold"
>
{overallTotal >= 0
? t('Total {{category}} Income', {
category: getLocalizedCategory(category, t),
})
? t('Total {{category}} Income', { category: categoryName })
: t('Total {{category}} Expense', {
category: getLocalizedCategory(category, t),
category: categoryName,

Check warning on line 158 in src/components/Reports/MPGAIncomeExpensesReport/BreakdownModal/BreakdownModal.tsx

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ Getting worse: Large Method

BreakdownModal:React.FC<BreakdownModalProps> increases from 143 to 145 lines of code, threshold = 120 Large functions with many lines of code are generally harder to understand and lower the code health. Avoid adding more lines to this function.
})}
</Typography>
</TableCell>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@ import userEvent from '@testing-library/user-event';
import { StaffExpenseCategoryEnum } from 'src/graphql/types.generated';
import { exportToCsv } from '../CustomExport/CustomExport';
import { ReportTypeEnum } from '../Helper/MPGAReportEnum';
import {
ContextType,
MPGAIncomeExpensesContext,
} from '../MPGAIncomeExpensesContext/MPGAIncomeExpensesContext';
import { MPGAIncomeExpensesReportTestWrapper } from '../MPGAIncomeExpensesReportTestWrapper';
import { MpgaTransactionsQuery } from '../MPGATransactions.generated';
import { mockData, months } from '../mockData';
import { ExportCsvButton } from './ExportCsvButton';

const mutationSpy = jest.fn();
Expand Down Expand Up @@ -173,11 +178,47 @@ describe('ExportCsvButton', () => {
expect(exportToCsv).not.toHaveBeenCalled();
});

it('disables all exports while the report is still loading', async () => {
// Rows can exist before the household has answered, and an export taken then would show a
// couple's salary as one row. Only the finished report is exportable.
const loadingContext = {
allData: mockData,
dataLoading: true,
monthLabels: months,
startBalance: null,
} as unknown as ContextType;

const { getByRole, findByRole } = render(
<MPGAIncomeExpensesContext.Provider value={loadingContext}>
<ExportCsvButton />
</MPGAIncomeExpensesContext.Provider>,
);

userEvent.click(getByRole('button', { name: 'Export CSV' }));

expect(
await findByRole('menuitem', { name: 'Income Report' }),
).toHaveAttribute('aria-disabled', 'true');
expect(getByRole('menuitem', { name: 'Expenses Report' })).toHaveAttribute(
'aria-disabled',
'true',
);
expect(getByRole('menuitem', { name: 'Balance Report' })).toHaveAttribute(
'aria-disabled',
'true',
);
});

it('closes the menu after an export is selected', async () => {
const { findByRole, queryByRole } = render(<TestComponent />);

userEvent.click(await findByRole('button', { name: 'Export CSV' }));
userEvent.click(await findByRole('menuitem', { name: 'Income Report' }));

const income = await findByRole('menuitem', { name: 'Income Report' });
await waitFor(() =>
Comment thread
frett marked this conversation as resolved.
expect(income).not.toHaveAttribute('aria-disabled', 'true'),
);
userEvent.click(income);

await waitFor(() =>
expect(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const ExportCsvButton: React.FC = () => {
const { t } = useTranslation();
const locale = useLocale();

const { allData: data, monthLabels } = useMPGAIncomeExpenses();
const { allData: data, dataLoading, monthLabels } = useMPGAIncomeExpenses();
const balanceData = useBalanceTableData();

return (
Expand All @@ -31,7 +31,8 @@ export const ExportCsvButton: React.FC = () => {
},
{
label: t('Income Report'),
disabled: !data.income.length,
// Rows can exist before the household answers, so only the finished report is exportable.
disabled: dataLoading || !data.income.length,
onClick: () =>
exportToCsv(
data.income,
Expand All @@ -42,7 +43,7 @@ export const ExportCsvButton: React.FC = () => {
},
{
label: t('Expenses Report'),
disabled: !data.expenses.length,
disabled: dataLoading || !data.expenses.length,
onClick: () =>
exportToCsv(
data.expenses,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ interface Transaction {
transactedAt: string;
description?: string | null;
amount: number;
/** Null when SAA has no employee for the transaction's EMPLID. */
personNumber?: string | null;
}

interface BreakdownByMonth {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const populateCardTableRows = (
openBreakdownModal: (breakdown: BreakdownTarget) => void,
) => {
const description: RenderCell = ({ row }) => {
const { category, transactions } = row;
const { category, person, transactions } = row;
return (
<Box display="flex" alignItems="center" width="100%">
<Tooltip title={row.description}>
Expand All @@ -26,7 +26,9 @@ export const populateCardTableRows = (
size="small"
sx={{ ml: 'auto', flexShrink: 0 }}
aria-label={t('View breakdown')}
onClick={() => openBreakdownModal({ category, transactions })}
onClick={() =>
openBreakdownModal({ category, person, transactions })
}
>
<InfoOutlined fontSize="small" />
</IconButton>
Expand Down
Loading
Loading