From 8c5c7c5a7a95d0fb1c925c3e9ef7a8e1f4385afd Mon Sep 17 00:00:00 2001 From: Caleb Cox Date: Fri, 4 Sep 2026 10:45:37 -0400 Subject: [PATCH 1/3] Show more details about gross salary and ASRs --- .../AdditionalSalaryRequest.graphql | 3 + .../AdditionalSalaryRequest.test.tsx | 2 + .../FormVersions/Edit/EditForm.test.tsx | 8 +- .../FormVersions/New/NewForm.test.tsx | 8 +- .../FormVersions/View/ViewForm.test.tsx | 8 +- .../FormVersions/testUtils.tsx | 5 +- .../Helper/SplitCapSubContent.test.tsx | 13 +-- .../RequestPage/RequestPage.test.tsx | 3 +- .../Shared/AdditionalSalaryRequestContext.tsx | 2 + .../useAdditionalSalaryRequestForm.test.tsx | 7 +- .../Shared/useAdditionalSalaryRequestForm.ts | 4 +- .../Shared/useSalaryCalculations.test.ts | 89 +++++++++++++------ .../Shared/useSalaryCalculations.ts | 18 ++-- .../SharedComponents/CurrentRequest.test.tsx | 2 + .../TotalSalaryRequested.test.tsx | 54 +++++++---- .../TotalSalaryRequested/TotalSalaryTable.tsx | 46 ++++++++-- .../RequestSummaryCard.test.tsx | 65 ++++++++++++-- .../RequestSummaryCard/RequestSummaryCard.tsx | 49 +++++++++- .../SalaryCalculation/useCaps.test.tsx | 81 +++++++++++++++++ .../SalaryCalculation/useCaps.ts | 6 ++ .../SalaryCalculation.graphql | 1 + 21 files changed, 372 insertions(+), 102 deletions(-) create mode 100644 src/components/HrTools/SalaryCalculator/SalaryCalculation/useCaps.test.tsx diff --git a/src/components/HrTools/AdditionalSalaryRequest/AdditionalSalaryRequest.graphql b/src/components/HrTools/AdditionalSalaryRequest/AdditionalSalaryRequest.graphql index 62efd39fec..53bada8b65 100644 --- a/src/components/HrTools/AdditionalSalaryRequest/AdditionalSalaryRequest.graphql +++ b/src/components/HrTools/AdditionalSalaryRequest/AdditionalSalaryRequest.graphql @@ -43,11 +43,14 @@ fragment AdditionalSalaryRequestDetailsFields on AdditionalSalaryRequest { combinedCap staffAccountBalance pendingAsrAmount + grossAnnualSalary + outstandingSalaryRequest } spouseCalculations { currentSalaryCap pendingAsrAmount staffAccountBalance + grossAnnualSalary } } diff --git a/src/components/HrTools/AdditionalSalaryRequest/AdditionalSalaryRequest.test.tsx b/src/components/HrTools/AdditionalSalaryRequest/AdditionalSalaryRequest.test.tsx index 384f687350..d7f8f2547f 100644 --- a/src/components/HrTools/AdditionalSalaryRequest/AdditionalSalaryRequest.test.tsx +++ b/src/components/HrTools/AdditionalSalaryRequest/AdditionalSalaryRequest.test.tsx @@ -41,6 +41,8 @@ const mockRequest = { currentSalaryCap: 50000, staffAccountBalance: 10000, pendingAsrAmount: 0, + grossAnnualSalary: 40000, + outstandingSalaryRequest: false, }, spouseCalculations: null, user: { diff --git a/src/components/HrTools/AdditionalSalaryRequest/FormVersions/Edit/EditForm.test.tsx b/src/components/HrTools/AdditionalSalaryRequest/FormVersions/Edit/EditForm.test.tsx index 90103f6384..cf9b8ea6f7 100644 --- a/src/components/HrTools/AdditionalSalaryRequest/FormVersions/Edit/EditForm.test.tsx +++ b/src/components/HrTools/AdditionalSalaryRequest/FormVersions/Edit/EditForm.test.tsx @@ -86,13 +86,7 @@ describe('EditForm', () => { }); it('should not render cap-related components when user does not exceed cap', () => { - const { queryByText } = renderComponent({ - contextOverrides: { - user: { - currentSalary: { grossSalaryAmount: 1000 }, - }, - }, - }); + const { queryByText } = renderComponent(); expect(queryByText('Total Salary Requested')).not.toBeInTheDocument(); expect(queryByText('Approval Process')).not.toBeInTheDocument(); diff --git a/src/components/HrTools/AdditionalSalaryRequest/FormVersions/New/NewForm.test.tsx b/src/components/HrTools/AdditionalSalaryRequest/FormVersions/New/NewForm.test.tsx index d95bc430c9..e1b5aa3574 100644 --- a/src/components/HrTools/AdditionalSalaryRequest/FormVersions/New/NewForm.test.tsx +++ b/src/components/HrTools/AdditionalSalaryRequest/FormVersions/New/NewForm.test.tsx @@ -136,13 +136,7 @@ describe('NewForm', () => { }); it('does not render exceeded cap components when not exceeded', () => { - const { queryByText } = renderComponent({ - contextOverrides: { - user: { - currentSalary: { grossSalaryAmount: 1000 }, - }, - }, - }); + const { queryByText } = renderComponent(); expect(queryByText('Total Salary Requested')).not.toBeInTheDocument(); expect(queryByText('Approval Process')).not.toBeInTheDocument(); diff --git a/src/components/HrTools/AdditionalSalaryRequest/FormVersions/View/ViewForm.test.tsx b/src/components/HrTools/AdditionalSalaryRequest/FormVersions/View/ViewForm.test.tsx index 577965c0d3..9611da49e7 100644 --- a/src/components/HrTools/AdditionalSalaryRequest/FormVersions/View/ViewForm.test.tsx +++ b/src/components/HrTools/AdditionalSalaryRequest/FormVersions/View/ViewForm.test.tsx @@ -90,13 +90,7 @@ describe('ViewForm', () => { }); it('should not render total salary requested or approval process when under cap', () => { - const { queryByText } = renderComponent({ - contextOverrides: { - user: { - currentSalary: { grossSalaryAmount: 1000 }, - }, - }, - }); + const { queryByText } = renderComponent(); expect(queryByText('Total Salary Requested')).not.toBeInTheDocument(); expect(queryByText('Approval Process')).not.toBeInTheDocument(); diff --git a/src/components/HrTools/AdditionalSalaryRequest/FormVersions/testUtils.tsx b/src/components/HrTools/AdditionalSalaryRequest/FormVersions/testUtils.tsx index 8fe85329ac..cbe43bfee3 100644 --- a/src/components/HrTools/AdditionalSalaryRequest/FormVersions/testUtils.tsx +++ b/src/components/HrTools/AdditionalSalaryRequest/FormVersions/testUtils.tsx @@ -21,9 +21,6 @@ export const mockUser = { personNumber: '00123456', emailAddress: 'john.doe@example.com', }, - currentSalary: { - grossSalaryAmount: 40000, - }, }; export const defaultMockContextValue = { @@ -35,6 +32,7 @@ export const defaultMockContextValue = { currentSalaryCap: 100000, staffAccountBalance: 40000, pendingAsrAmount: 5000, + grossAnnualSalary: 40000, }, }, }, @@ -42,6 +40,7 @@ export const defaultMockContextValue = { currentSalaryCap: 100000, staffAccountBalance: 40000, pendingAsrAmount: 5000, + grossAnnualSalary: 40000, }, user: mockUser, }; diff --git a/src/components/HrTools/AdditionalSalaryRequest/RequestPage/Helper/SplitCapSubContent.test.tsx b/src/components/HrTools/AdditionalSalaryRequest/RequestPage/Helper/SplitCapSubContent.test.tsx index 28de9e9c86..66ac592280 100644 --- a/src/components/HrTools/AdditionalSalaryRequest/RequestPage/Helper/SplitCapSubContent.test.tsx +++ b/src/components/HrTools/AdditionalSalaryRequest/RequestPage/Helper/SplitCapSubContent.test.tsx @@ -36,17 +36,18 @@ const setupContext = (opts: { mockUseAdditionalSalaryRequest.mockReturnValue({ traditional403bPercentage: 0.12, roth403bPercentage: 0.1, - user: { currentSalary: { grossSalaryAmount: 50000 } }, - spouse: - spouseCap !== null - ? { currentSalary: { grossSalaryAmount: spouseGross } } - : undefined, + spouse: spouseCap !== null ? {} : undefined, requestData: { latestAdditionalSalaryRequest: { - calculations: { currentSalaryCap: 60000, pendingAsrAmount: 0 }, + calculations: { + grossAnnualSalary: 50000, + currentSalaryCap: 60000, + pendingAsrAmount: 0, + }, spouseCalculations: spouseCap !== null ? { + grossAnnualSalary: spouseGross, currentSalaryCap: spouseCap, pendingAsrAmount: spousePending, } diff --git a/src/components/HrTools/AdditionalSalaryRequest/RequestPage/RequestPage.test.tsx b/src/components/HrTools/AdditionalSalaryRequest/RequestPage/RequestPage.test.tsx index b2fa4fcbc3..b1263e915f 100644 --- a/src/components/HrTools/AdditionalSalaryRequest/RequestPage/RequestPage.test.tsx +++ b/src/components/HrTools/AdditionalSalaryRequest/RequestPage/RequestPage.test.tsx @@ -497,7 +497,6 @@ describe('RequestPage', () => { currentStep: AdditionalSalaryRequestSectionEnum.CompleteForm, pageType: PageEnum.New, spouse: { - currentSalary: { grossSalaryAmount: 40000 }, staffInfo: { preferredName: 'Jane', lastName: 'Doe', @@ -506,9 +505,11 @@ describe('RequestPage', () => { requestData: { latestAdditionalSalaryRequest: { calculations: { + grossAnnualSalary: 40000, currentSalaryCap: 500, }, spouseCalculations: { + grossAnnualSalary: 40000, currentSalaryCap: 500, pendingAsrAmount: 600, }, diff --git a/src/components/HrTools/AdditionalSalaryRequest/Shared/AdditionalSalaryRequestContext.tsx b/src/components/HrTools/AdditionalSalaryRequest/Shared/AdditionalSalaryRequestContext.tsx index cc6fd22080..89cbbf0e76 100644 --- a/src/components/HrTools/AdditionalSalaryRequest/Shared/AdditionalSalaryRequestContext.tsx +++ b/src/components/HrTools/AdditionalSalaryRequest/Shared/AdditionalSalaryRequestContext.tsx @@ -108,6 +108,8 @@ export type AdditionalSalaryRequestType = { | 'combinedCap' | 'staffAccountBalance' | 'pendingAsrAmount' + | 'grossAnnualSalary' + | 'outstandingSalaryRequest' >; user: HcmQuery['hcm'][0] | undefined; spouse: HcmQuery['hcm'][1] | undefined; diff --git a/src/components/HrTools/AdditionalSalaryRequest/Shared/useAdditionalSalaryRequestForm.test.tsx b/src/components/HrTools/AdditionalSalaryRequest/Shared/useAdditionalSalaryRequestForm.test.tsx index 816576afcb..b578e8397c 100644 --- a/src/components/HrTools/AdditionalSalaryRequest/Shared/useAdditionalSalaryRequestForm.test.tsx +++ b/src/components/HrTools/AdditionalSalaryRequest/Shared/useAdditionalSalaryRequestForm.test.tsx @@ -69,6 +69,7 @@ const defaultMockContextValue = { currentSalaryCap: 100000, staffAccountBalance: 50000, pendingAsrAmount: 0, + grossAnnualSalary: 40000, }, }, }, @@ -93,9 +94,6 @@ const defaultMockContextValue = { personNumber: '00123456', emailAddress: 'john.doe@example.com', }, - currentSalary: { - grossSalaryAmount: 40000, - }, }, ], }, @@ -161,9 +159,12 @@ const defaultGqlMocks: DeepPartial = { AdditionalSalaryRequest: { latestAdditionalSalaryRequest: { id: 'test-request-id', + progressiveApprovalTierReason: + ProgressiveApprovalTierReasonEnum.OverUserCap, calculations: { currentSalaryCap: 100000, pendingAsrAmount: 0, + grossAnnualSalary: 40000, }, }, }, diff --git a/src/components/HrTools/AdditionalSalaryRequest/Shared/useAdditionalSalaryRequestForm.ts b/src/components/HrTools/AdditionalSalaryRequest/Shared/useAdditionalSalaryRequestForm.ts index 011aa86cc6..b1e26fce8d 100644 --- a/src/components/HrTools/AdditionalSalaryRequest/Shared/useAdditionalSalaryRequestForm.ts +++ b/src/components/HrTools/AdditionalSalaryRequest/Shared/useAdditionalSalaryRequestForm.ts @@ -47,7 +47,9 @@ export const useAdditionalSalaryRequestForm = ( const pendingAsrAmount = requestData?.latestAdditionalSalaryRequest?.calculations.pendingAsrAmount ?? 0; - const grossAnnualSalary = user?.currentSalary?.grossSalaryAmount ?? 0; + const grossAnnualSalary = + requestData?.latestAdditionalSalaryRequest?.calculations + .grossAnnualSalary ?? 0; const [updateAdditionalSalaryRequest] = useUpdateAdditionalSalaryRequestMutation(); diff --git a/src/components/HrTools/AdditionalSalaryRequest/Shared/useSalaryCalculations.test.ts b/src/components/HrTools/AdditionalSalaryRequest/Shared/useSalaryCalculations.test.ts index 17dcc55f9b..c61a5a0225 100644 --- a/src/components/HrTools/AdditionalSalaryRequest/Shared/useSalaryCalculations.test.ts +++ b/src/components/HrTools/AdditionalSalaryRequest/Shared/useSalaryCalculations.test.ts @@ -220,12 +220,10 @@ describe('useSalaryCalculations', () => { mockUseAdditionalSalaryRequest.mockReturnValue({ traditional403bPercentage: 0.12, roth403bPercentage: 0.1, - user: { - currentSalary: { grossSalaryAmount: 1000 }, - }, requestData: { latestAdditionalSalaryRequest: { calculations: { + grossAnnualSalary: 1000, currentSalaryCap: 5000, pendingAsrAmount: 1000, }, @@ -260,12 +258,10 @@ describe('useSalaryCalculations', () => { mockUseAdditionalSalaryRequest.mockReturnValue({ traditional403bPercentage: 0.12, roth403bPercentage: 0.1, - user: { - currentSalary: { grossSalaryAmount: 50000 }, - }, requestData: { latestAdditionalSalaryRequest: { calculations: { + grossAnnualSalary: 50000, currentSalaryCap: 10000, pendingAsrAmount: 10000, }, @@ -295,14 +291,46 @@ describe('useSalaryCalculations', () => { expect(result.current.exceedsCap).toBe(true); }); + it('uses the calculated gross annual salary', () => { + mockUseAdditionalSalaryRequest.mockReturnValue({ + traditional403bPercentage: 0.12, + roth403bPercentage: 0.1, + requestData: { + latestAdditionalSalaryRequest: { + calculations: { + grossAnnualSalary: 72000, + currentSalaryCap: 80000, + pendingAsrAmount: 0, + }, + }, + }, + } as unknown as ReturnType); + + const values: CompleteFormValues = { + ...baseValues, + additionalSalaryWithinMax: '10000', + }; + + const { result } = renderHook(() => useSalaryCalculations({ values }), { + wrapper: ({ children }) => FormikWrapper({ children, values }), + }); + + // 72000 + 0 + 10000 = 82000, over the 80000 cap; the HCM salary would have given 60000 + expect(result.current.requestedAnnualSalary).toBe(82000); + expect(result.current.exceedsCap).toBe(true); + }); + it('excludes current-year backpay from requestedAnnualSalary but not from total', () => { mockUseAdditionalSalaryRequest.mockReturnValue({ traditional403bPercentage: 0.12, roth403bPercentage: 0.1, - user: { currentSalary: { grossSalaryAmount: 50000 } }, requestData: { latestAdditionalSalaryRequest: { - calculations: { currentSalaryCap: 60000, pendingAsrAmount: 0 }, + calculations: { + grossAnnualSalary: 50000, + currentSalaryCap: 60000, + pendingAsrAmount: 0, + }, }, }, } as unknown as ReturnType); @@ -333,14 +361,17 @@ describe('useSalaryCalculations', () => { mockUseAdditionalSalaryRequest.mockReturnValue({ traditional403bPercentage: 0.12, roth403bPercentage: 0.1, - user: { currentSalary: { grossSalaryAmount: 50000 } }, - spouse: { currentSalary: { grossSalaryAmount: 40000 } }, + spouse: {}, requestData: { latestAdditionalSalaryRequest: { calculations: { + grossAnnualSalary: 50000, currentSalaryCap: 70000, }, - spouseCalculations, + spouseCalculations: { + grossAnnualSalary: 40000, + ...spouseCalculations, + }, }, }, } as unknown as ReturnType); @@ -434,14 +465,17 @@ describe('useSalaryCalculations', () => { mockUseAdditionalSalaryRequest.mockReturnValue({ traditional403bPercentage: 0.12, roth403bPercentage: 0.1, - user: { currentSalary: { grossSalaryAmount: 50000 } }, - spouse: { currentSalary: { grossSalaryAmount: 40000 } }, + spouse: {}, requestData: { latestAdditionalSalaryRequest: { calculations: { + grossAnnualSalary: 50000, currentSalaryCap: 60000, }, - spouseCalculations, + spouseCalculations: { + grossAnnualSalary: 40000, + ...spouseCalculations, + }, progressiveApprovalTier, }, }, @@ -589,14 +623,17 @@ describe('useSalaryCalculations', () => { mockUseAdditionalSalaryRequest.mockReturnValue({ traditional403bPercentage: 0.12, roth403bPercentage: 0.1, - user: { currentSalary: { grossSalaryAmount: 50000 } }, - spouse: { currentSalary: { grossSalaryAmount: 40000 } }, + spouse: {}, requestData: { latestAdditionalSalaryRequest: { calculations: { + grossAnnualSalary: 50000, currentSalaryCap: 55000, }, - spouseCalculations, + spouseCalculations: { + grossAnnualSalary: 40000, + ...spouseCalculations, + }, progressiveApprovalTier, }, }, @@ -692,11 +729,10 @@ describe('useSalaryCalculations', () => { mockUseAdditionalSalaryRequest.mockReturnValue({ traditional403bPercentage: 0.12, roth403bPercentage: 0.1, - user: { currentSalary: { grossSalaryAmount: 50000 } }, spouse: undefined, requestData: { latestAdditionalSalaryRequest: { - calculations: { currentSalaryCap: cap }, + calculations: { grossAnnualSalary: 50000, currentSalaryCap: cap }, progressiveApprovalTier, }, }, @@ -766,12 +802,12 @@ describe('useSalaryCalculations', () => { mockUseAdditionalSalaryRequest.mockReturnValue({ traditional403bPercentage: 0.12, roth403bPercentage: 0.1, - user: { currentSalary: { grossSalaryAmount: 50000 } }, - spouse: { currentSalary: { grossSalaryAmount: 40000 } }, + spouse: {}, requestData: { latestAdditionalSalaryRequest: { - calculations: { currentSalaryCap: 60000 }, + calculations: { grossAnnualSalary: 50000, currentSalaryCap: 60000 }, spouseCalculations: { + grossAnnualSalary: 40000, currentSalaryCap: 50000, pendingAsrAmount: 2000, }, @@ -796,12 +832,12 @@ describe('useSalaryCalculations', () => { mockUseAdditionalSalaryRequest.mockReturnValue({ traditional403bPercentage: 0.12, roth403bPercentage: 0.1, - user: { currentSalary: { grossSalaryAmount: 50000 } }, - spouse: { currentSalary: { grossSalaryAmount: 40000 } }, + spouse: {}, requestData: { latestAdditionalSalaryRequest: { - calculations: { currentSalaryCap: 60000 }, + calculations: { grossAnnualSalary: 50000, currentSalaryCap: 60000 }, spouseCalculations: { + grossAnnualSalary: 40000, currentSalaryCap: 50000, pendingAsrAmount: 15000, }, @@ -826,11 +862,10 @@ describe('useSalaryCalculations', () => { mockUseAdditionalSalaryRequest.mockReturnValue({ traditional403bPercentage: 0.12, roth403bPercentage: 0.1, - user: { currentSalary: { grossSalaryAmount: 50000 } }, spouse: undefined, requestData: { latestAdditionalSalaryRequest: { - calculations: { currentSalaryCap: 60000 }, + calculations: { grossAnnualSalary: 50000, currentSalaryCap: 60000 }, }, }, } as unknown as ReturnType); diff --git a/src/components/HrTools/AdditionalSalaryRequest/Shared/useSalaryCalculations.ts b/src/components/HrTools/AdditionalSalaryRequest/Shared/useSalaryCalculations.ts index f4bcf721d8..6c84a1af96 100644 --- a/src/components/HrTools/AdditionalSalaryRequest/Shared/useSalaryCalculations.ts +++ b/src/components/HrTools/AdditionalSalaryRequest/Shared/useSalaryCalculations.ts @@ -73,13 +73,8 @@ const calculate403bDeductions = ( export const useSalaryCalculations = ({ values, }: UseSalaryCalculationsProps): SalaryCalculations => { - const { - traditional403bPercentage, - roth403bPercentage, - requestData, - user, - spouse, - } = useAdditionalSalaryRequest(); + const { traditional403bPercentage, roth403bPercentage, requestData, spouse } = + useAdditionalSalaryRequest(); const individualCap = requestData?.latestAdditionalSalaryRequest?.calculations.currentSalaryCap ?? 0; @@ -88,8 +83,13 @@ export const useSalaryCalculations = ({ ?.currentSalaryCap ?? null) : null; - const grossAnnualSalary = user?.currentSalary?.grossSalaryAmount ?? 0; - const spouseGrossAnnualSalary = spouse?.currentSalary?.grossSalaryAmount ?? 0; + const grossAnnualSalary = + requestData?.latestAdditionalSalaryRequest?.calculations + .grossAnnualSalary ?? 0; + const spouseGrossAnnualSalary = spouse + ? (requestData?.latestAdditionalSalaryRequest?.spouseCalculations + ?.grossAnnualSalary ?? 0) + : 0; return useMemo(() => { const total = getTotal(values); diff --git a/src/components/HrTools/AdditionalSalaryRequest/SharedComponents/CurrentRequest.test.tsx b/src/components/HrTools/AdditionalSalaryRequest/SharedComponents/CurrentRequest.test.tsx index 2b7a1d9662..a7f09b0164 100644 --- a/src/components/HrTools/AdditionalSalaryRequest/SharedComponents/CurrentRequest.test.tsx +++ b/src/components/HrTools/AdditionalSalaryRequest/SharedComponents/CurrentRequest.test.tsx @@ -57,6 +57,8 @@ const mockRequest: RequestType = { currentSalaryCap: 50000, staffAccountBalance: 10000, pendingAsrAmount: 0, + grossAnnualSalary: 40000, + outstandingSalaryRequest: false, }, spouseCalculations: null, }; diff --git a/src/components/HrTools/AdditionalSalaryRequest/SubmitModalAccordions/TotalSalaryRequested/TotalSalaryRequested.test.tsx b/src/components/HrTools/AdditionalSalaryRequest/SubmitModalAccordions/TotalSalaryRequested/TotalSalaryRequested.test.tsx index c0f0f634f6..0a5ef41fc9 100644 --- a/src/components/HrTools/AdditionalSalaryRequest/SubmitModalAccordions/TotalSalaryRequested/TotalSalaryRequested.test.tsx +++ b/src/components/HrTools/AdditionalSalaryRequest/SubmitModalAccordions/TotalSalaryRequested/TotalSalaryRequested.test.tsx @@ -32,17 +32,14 @@ const defaultMockContextValue = { calculations: { currentSalaryCap: 100000, pendingAsrAmount: 5000, + grossAnnualSalary: 50000, }, }, }, calculations: { currentSalaryCap: 100000, pendingAsrAmount: 5000, - }, - user: { - currentSalary: { - grossSalaryAmount: 50000, - }, + grossAnnualSalary: 50000, }, }; @@ -95,8 +92,40 @@ describe('TotalSalaryRequested', () => { jest.clearAllMocks(); }); + it('explains that the figures include an outstanding salary request', async () => { + const { getAllByLabelText, getByRole } = renderComponent({ + contextOverrides: { + calculations: { + ...defaultMockContextValue.calculations, + outstandingSalaryRequest: true, + }, + }, + }); + + userEvent.click(getByRole('button', { name: 'Expand salary details' })); + + // One tooltip beside Gross Annual Salary and one beside Total Salary Requested + await waitFor(() => + expect( + getAllByLabelText('Includes your pending Salary Calculation Request'), + ).toHaveLength(2), + ); + }); + + it('omits the salary request explanation when there is no outstanding request', async () => { + const { getByRole, queryByLabelText } = renderComponent(); + + userEvent.click(getByRole('button', { name: 'Expand salary details' })); + + await waitFor(() => expect(getByRole('table')).toBeInTheDocument()); + expect( + queryByLabelText('Includes your pending Salary Calculation Request'), + ).not.toBeInTheDocument(); + }); + it('renders accordion with title, table, and summary items', async () => { - const { getByText, queryByTestId, getByRole } = renderComponent(); + const { getAllByText, getByText, queryByTestId, getByRole } = + renderComponent(); expect(queryByTestId('card')).not.toBeInTheDocument(); @@ -124,17 +153,12 @@ describe('TotalSalaryRequested', () => { expect(getByText('Maximum Allowable Salary')).toBeInTheDocument(); expect(getByText('Gross Annual Salary')).toBeInTheDocument(); expect( - getByText('Additional Salary Received This Year'), + getByText('Additional Salary Previously Requested This Year'), ).toBeInTheDocument(); - expect(getByText('Additional Salary on this Request')).toBeInTheDocument(); + expect(getByText('Additional Salary on This Request')).toBeInTheDocument(); expect(getByText('Total Salary Requested:')).toBeInTheDocument(); - expect( - getByText('Does not include payments received for backpay.'), - ).toBeInTheDocument(); - expect( - getByText('Does not include requests made for backpay.'), - ).toBeInTheDocument(); + expect(getAllByText('Does not include backpay for 2020.')).toHaveLength(2); }); it('renders card version when onForm is true', () => { @@ -205,7 +229,7 @@ describe('TotalSalaryRequested', () => { ).toBeInTheDocument(); }); - // Additional salary on this request should be $10,000.00 + // Additional Salary on This Request should be $10,000.00 expect(getAllByText('$10,000.00').length).toBeGreaterThanOrEqual(1); }); }); diff --git a/src/components/HrTools/AdditionalSalaryRequest/SubmitModalAccordions/TotalSalaryRequested/TotalSalaryTable.tsx b/src/components/HrTools/AdditionalSalaryRequest/SubmitModalAccordions/TotalSalaryRequested/TotalSalaryTable.tsx index a7022fa11d..f792fb404e 100644 --- a/src/components/HrTools/AdditionalSalaryRequest/SubmitModalAccordions/TotalSalaryRequested/TotalSalaryTable.tsx +++ b/src/components/HrTools/AdditionalSalaryRequest/SubmitModalAccordions/TotalSalaryRequested/TotalSalaryTable.tsx @@ -1,14 +1,17 @@ import { useMemo } from 'react'; +import InfoIcon from '@mui/icons-material/Info'; import { Table, TableBody, TableCell, TableHead, TableRow, + Tooltip, Typography, styled, } from '@mui/material'; import { useFormikContext } from 'formik'; +import { DateTime } from 'luxon'; import { useTranslation } from 'react-i18next'; import { useLocale } from 'src/hooks/useLocale'; import { currencyFormat } from 'src/lib/intlFormat'; @@ -26,6 +29,14 @@ const StyledAmountTableCell = styled(TableCell)(() => ({ width: '30%', })); +const InfoTooltipIcon = styled(InfoIcon)(({ theme }) => ({ + marginLeft: theme.spacing(0.5), + verticalAlign: 'middle', + cursor: 'pointer', + color: theme.palette.mpdxGrayDark.main, + fontSize: '1rem', +})); + export const TotalSalaryTable: React.FC = () => { const { t } = useTranslation(); const locale = useLocale(); @@ -35,6 +46,13 @@ export const TotalSalaryTable: React.FC = () => { const { values } = useFormikContext(); const individualCap = calculations?.currentSalaryCap ?? 0; + const outstandingSalaryRequest = calculations?.outstandingSalaryRequest; + const salaryRequestTooltip = t( + 'Includes your pending Salary Calculation Request', + ); + const backpayExplanation = t('Does not include backpay for {{year}}.', { + year: DateTime.local().year, + }); const { nonBackpayTotal, @@ -55,18 +73,19 @@ export const TotalSalaryTable: React.FC = () => { { id: 'grossAnnual', label: t('Gross Annual Salary'), + tooltip: outstandingSalaryRequest ? salaryRequestTooltip : undefined, value: grossAnnualSalary, }, { id: 'additionalReceived', - label: t('Additional Salary Received This Year'), - description: t('Does not include payments received for backpay.'), + label: t('Additional Salary Previously Requested This Year'), + description: backpayExplanation, value: additionalSalaryReceivedThisYear, }, { id: 'additionalRequested', - label: t('Additional Salary on this Request'), - description: t('Does not include requests made for backpay.'), + label: t('Additional Salary on This Request'), + description: backpayExplanation, value: nonBackpayTotal, }, ], @@ -76,6 +95,9 @@ export const TotalSalaryTable: React.FC = () => { grossAnnualSalary, additionalSalaryReceivedThisYear, nonBackpayTotal, + outstandingSalaryRequest, + salaryRequestTooltip, + backpayExplanation, ], ); @@ -92,10 +114,17 @@ export const TotalSalaryTable: React.FC = () => { - {summaryItems.map(({ id, label, description, value }) => ( + {summaryItems.map(({ id, label, description, tooltip, value }) => ( - {label} + + {label} + {tooltip && ( + + + + )} + {description && ( {description} @@ -118,6 +147,11 @@ export const TotalSalaryTable: React.FC = () => { {t('Total Salary Requested:')} + {outstandingSalaryRequest && ( + + + + )} diff --git a/src/components/HrTools/SalaryCalculator/SalaryCalculation/RequestSummaryCard/RequestSummaryCard.test.tsx b/src/components/HrTools/SalaryCalculator/SalaryCalculation/RequestSummaryCard/RequestSummaryCard.test.tsx index 82c92f7ef7..b58ea224ec 100644 --- a/src/components/HrTools/SalaryCalculator/SalaryCalculation/RequestSummaryCard/RequestSummaryCard.test.tsx +++ b/src/components/HrTools/SalaryCalculator/SalaryCalculation/RequestSummaryCard/RequestSummaryCard.test.tsx @@ -20,13 +20,15 @@ const defaultSalaryMock: DeepPartial = requestedSeca: 10002, contributing403bAmount: 10003, requestedGross: 10004, - effectiveCap: 10005, + requestedYtdGross: 10005, + effectiveCap: 10006, }, spouseCalculations: { requestedSeca: 20002, contributing403bAmount: 20003, requestedGross: 20004, - effectiveCap: 20005, + requestedYtdGross: 20005, + effectiveCap: 20006, }, }; @@ -101,7 +103,7 @@ We'll forward your request to them and get back to you with their decision.", expect(getByTestId('RequestSummaryCard-status')).toHaveTextContent( "Your Combined Gross Requested Salary is within your Combined Maximum Allowable Salary. \ However, John's Gross Requested Salary exceeds their individual Maximum Allowable Salary. \ -If this is correct, please provide reasoning for why John's Requested Salary should exceed $10,005.00 in the Additional Information section below \ +If this is correct, please provide reasoning for why John's Requested Salary should exceed $10,006.00 in the Additional Information section below \ or make changes to how your Requested Salary is distributed above.", ), ); @@ -126,7 +128,7 @@ or make changes to how your Requested Salary is distributed above.", expect(getByTestId('RequestSummaryCard-status')).toHaveTextContent( "Your Combined Gross Requested Salary is within your Combined Maximum Allowable Salary. \ However, Jane's Gross Requested Salary exceeds their individual Maximum Allowable Salary. \ -If this is correct, please provide reasoning for why Jane's Requested Salary should exceed $20,005.00 in the Additional Information section below \ +If this is correct, please provide reasoning for why Jane's Requested Salary should exceed $20,006.00 in the Additional Information section below \ or make changes to how your Requested Salary is distributed above.", ), ); @@ -167,7 +169,7 @@ This may affect your selected effective date.', expect( getByTestId('RequestSummaryCard-requestedVsMax'), ).toHaveTextContent( - 'Combined Gross Salary / Max Allowable Salary$30,008.00 / $30,010.00', + 'Combined Gross Salary / Max Allowable Salary$30,010.00 / $30,012.00', ), ); }); @@ -182,6 +184,24 @@ This may affect your selected effective date.', ); }); + it('compares the YTD gross against the cap', async () => { + const { getByTestId } = render( + , + ); + + await waitFor(() => + expect( + getByTestId('RequestSummaryCard-requestedVsMax'), + ).toHaveTextContent('$12,000.00 / $10,006.00'), + ); + expect(getByTestId('RequestSummaryCard-remaining')).toHaveTextContent( + '-$1,994.00', + ); + }); + describe('table', () => { it('renders table headers, row headers, and cells', async () => { const { getByRole } = render(); @@ -194,6 +214,8 @@ This may affect your selected effective date.', 'SECA and Related Federal Taxes', '403b Contribution', 'Gross Requested Salary', + 'Additional Salary Requested This YearDoes not include backpay for 2020.', + 'Total Gross Salary This Year', 'Maximum Allowable Salary', ], cells: [ @@ -201,7 +223,34 @@ This may affect your selected effective date.', ['$10,002.00', '$20,002.00'], ['$10,003.00', '$20,003.00'], ['$10,004.00', '$20,004.00'], + ['$1.00', '$1.00'], ['$10,005.00', '$20,005.00'], + ['$10,006.00', '$20,006.00'], + ], + }), + ); + }); + + it('shows the additional salary bridging the gross and YTD gross', async () => { + const { getByRole } = render( + , + ); + + await waitFor(() => + expect(getByRole('table')).toHaveTableStructure({ + cells: [ + '$10,001.00', + '$10,002.00', + '$10,003.00', + '$60,000.00', + '$15,000.00', + '$75,000.00', + '$10,006.00', ], }), ); @@ -234,7 +283,7 @@ This may affect your selected effective date.', expect( getByTestId('RequestSummaryCard-requestedVsMax'), ).toHaveTextContent( - 'Your Gross Requested Salary / Max Allowable Salary$10,004.00 / $10,005.00', + 'Your Gross Requested Salary / Max Allowable Salary$10,005.00 / $10,006.00', ), ); @@ -250,6 +299,8 @@ This may affect your selected effective date.', 'SECA and Related Federal Taxes', '403b Contribution', 'Gross Requested Salary', + 'Additional Salary Requested This YearDoes not include backpay for 2020.', + 'Total Gross Salary This Year', 'Maximum Allowable Salary', ], cells: [ @@ -257,7 +308,9 @@ This may affect your selected effective date.', '$10,002.00', '$10,003.00', '$10,004.00', + '$1.00', '$10,005.00', + '$10,006.00', ], }), ); diff --git a/src/components/HrTools/SalaryCalculator/SalaryCalculation/RequestSummaryCard/RequestSummaryCard.tsx b/src/components/HrTools/SalaryCalculator/SalaryCalculation/RequestSummaryCard/RequestSummaryCard.tsx index f97b1c8715..ed3ffc3029 100644 --- a/src/components/HrTools/SalaryCalculator/SalaryCalculation/RequestSummaryCard/RequestSummaryCard.tsx +++ b/src/components/HrTools/SalaryCalculator/SalaryCalculation/RequestSummaryCard/RequestSummaryCard.tsx @@ -14,6 +14,7 @@ import { styled, useTheme, } from '@mui/material'; +import { DateTime } from 'luxon'; import { Trans, useTranslation } from 'react-i18next'; import { useFormatters } from 'src/components/HrTools/Shared/useFormatters'; import { @@ -66,6 +67,7 @@ export const RequestSummaryCard: React.FC = () => { progressiveApprovalTier?.tier !== ProgressiveApprovalTierEnum.DivisionHead; const { combinedGross, + combinedYtdGross, combinedEffectiveCap: combinedCap, overCapPerson, } = useCaps(); @@ -81,6 +83,10 @@ export const RequestSummaryCard: React.FC = () => { const combined403b = (calcs?.contributing403bAmount ?? 0) + (spouseCalcs?.contributing403bAmount ?? 0); + const asrAmount = + (calcs?.requestedYtdGross ?? 0) - (calcs?.requestedGross ?? 0); + const spouseAsrAmount = + (spouseCalcs?.requestedYtdGross ?? 0) - (spouseCalcs?.requestedGross ?? 0); const categories: Category[] = [ { @@ -182,7 +188,7 @@ export const RequestSummaryCard: React.FC = () => { aria-describedby={requestedVsMaxId} className={approvalRequired ? 'invalid' : undefined} > - {formatCurrency(combinedGross)} / {formatCurrency(combinedCap)} + {formatCurrency(combinedYtdGross)} / {formatCurrency(combinedCap)} @@ -206,7 +212,7 @@ export const RequestSummaryCard: React.FC = () => { id={remainingId} className={approvalRequired ? 'invalid' : undefined} > - {formatCurrency(combinedCap - combinedGross)} + {formatCurrency(combinedCap - combinedYtdGross)} @@ -288,14 +294,49 @@ export const RequestSummaryCard: React.FC = () => { + {formatCurrency(calcs?.requestedGross)} + {hasSpouse && ( + + {formatCurrency(spouseCalcs.requestedGross)} + + )} + + + + + {t('Additional Salary Requested This Year')} + + {t('Does not include backpay for {{year}}.', { + year: DateTime.local().year, + })} + + + {formatCurrency(asrAmount)} + {hasSpouse && ( + {formatCurrency(spouseAsrAmount)} + )} + + + + + {t('Total Gross Salary This Year')} + + + + - {formatCurrency(calcs?.requestedGross)} + {formatCurrency(calcs?.requestedYtdGross)} {hasSpouse && ( - {formatCurrency(spouseCalcs.requestedGross)} + {formatCurrency(spouseCalcs.requestedYtdGross)} )} diff --git a/src/components/HrTools/SalaryCalculator/SalaryCalculation/useCaps.test.tsx b/src/components/HrTools/SalaryCalculator/SalaryCalculation/useCaps.test.tsx new file mode 100644 index 0000000000..ff029193f1 --- /dev/null +++ b/src/components/HrTools/SalaryCalculator/SalaryCalculation/useCaps.test.tsx @@ -0,0 +1,81 @@ +import { renderHook, waitFor } from '@testing-library/react'; +import { ProgressiveApprovalTierReasonEnum } from 'src/graphql/types.generated'; +import { + SalaryCalculatorTestWrapper, + SalaryCalculatorTestWrapperProps, +} from '../SalaryCalculatorTestWrapper'; +import { useCaps } from './useCaps'; + +const renderUseCaps = ( + salaryRequestMock: SalaryCalculatorTestWrapperProps['salaryRequestMock'], + hasSpouse = true, +) => + renderHook(() => useCaps(), { + wrapper: ({ children }) => ( + + {children} + + ), + }); + +describe('useCaps', () => { + it('sums the requested gross salaries', async () => { + const { result } = renderUseCaps({ + calculations: { requestedGross: 50000, requestedYtdGross: 55000 }, + spouseCalculations: { requestedGross: 30000, requestedYtdGross: 32000 }, + }); + + await waitFor(() => expect(result.current.combinedGross).toBe(80000)); + }); + + it('sums the requested YTD gross salaries separately from the requested gross', async () => { + const { result } = renderUseCaps({ + calculations: { requestedGross: 50000, requestedYtdGross: 55000 }, + spouseCalculations: { requestedGross: 30000, requestedYtdGross: 32000 }, + }); + + await waitFor(() => expect(result.current.combinedYtdGross).toBe(87000)); + }); + + it('sums the effective caps', async () => { + const { result } = renderUseCaps({ + calculations: { effectiveCap: 60000 }, + spouseCalculations: { effectiveCap: 40000 }, + }); + + await waitFor(() => + expect(result.current.combinedEffectiveCap).toBe(100000), + ); + }); + + it('ignores the spouse when there is none', async () => { + const { result } = renderUseCaps( + { calculations: { requestedGross: 50000, requestedYtdGross: 55000 } }, + false, + ); + + await waitFor(() => expect(result.current.combinedGross).toBe(50000)); + expect(result.current.combinedYtdGross).toBe(55000); + }); + + it('names the person who is over their cap', async () => { + const { result } = renderUseCaps({ + progressiveApprovalTierReason: + ProgressiveApprovalTierReasonEnum.OverSpouseCap, + spouseCalculations: { effectiveCap: 40000 }, + }); + + await waitFor(() => + expect(result.current.overCapPerson?.effectiveCap).toBe('$40,000.00'), + ); + }); + + it('has no over-cap person when no cap was exceeded', async () => { + const { result } = renderUseCaps({ progressiveApprovalTierReason: null }); + + await waitFor(() => expect(result.current.overCapPerson).toBeNull()); + }); +}); diff --git a/src/components/HrTools/SalaryCalculator/SalaryCalculation/useCaps.ts b/src/components/HrTools/SalaryCalculator/SalaryCalculation/useCaps.ts index 013226d265..9481074f46 100644 --- a/src/components/HrTools/SalaryCalculator/SalaryCalculation/useCaps.ts +++ b/src/components/HrTools/SalaryCalculator/SalaryCalculation/useCaps.ts @@ -14,6 +14,9 @@ interface UseCapsResult { /** The sum of the users' requested gross salaries */ combinedGross: number; + /** The sum of the users' requested year-to-date gross salaries, which includes ASRs */ + combinedYtdGross: number; + /** The sum of the users' effective caps */ combinedEffectiveCap: number; @@ -31,6 +34,8 @@ export const useCaps = (): UseCapsResult => { const combinedGross = (calcs?.requestedGross ?? 0) + (spouseCalcs?.requestedGross ?? 0); + const combinedYtdGross = + (calcs?.requestedYtdGross ?? 0) + (spouseCalcs?.requestedYtdGross ?? 0); const combinedEffectiveCap = (calcs?.effectiveCap ?? 0) + (spouseCalcs?.effectiveCap ?? 0); @@ -50,6 +55,7 @@ export const useCaps = (): UseCapsResult => { return { combinedGross, + combinedYtdGross, combinedEffectiveCap, overCapPerson, }; diff --git a/src/components/HrTools/SalaryCalculator/SalaryCalculatorContext/SalaryCalculation.graphql b/src/components/HrTools/SalaryCalculator/SalaryCalculatorContext/SalaryCalculation.graphql index 742c841d16..938af05412 100644 --- a/src/components/HrTools/SalaryCalculator/SalaryCalculatorContext/SalaryCalculation.graphql +++ b/src/components/HrTools/SalaryCalculator/SalaryCalculatorContext/SalaryCalculation.graphql @@ -56,6 +56,7 @@ fragment CalculationFields on SalaryRequestCalculations { requestedSeca requestedWithSeca requestedGross + requestedYtdGross requestedAboveMinimum requestedMha requestedAboveMha From af523bda5c2765320b0e04ac000ccd6a99f7f357 Mon Sep 17 00:00:00 2001 From: Caleb Cox Date: Fri, 4 Sep 2026 11:10:31 -0400 Subject: [PATCH 2/3] Replace pendingAsrAmount with ytdAsrAmount MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pendingAsrAmount has always held the year's entire ASR total — pending, action required, approved, and paid alike — so the name implied the opposite of what it returned. The API now exposes it as ytdAsrAmount and keeps pendingAsrAmount only as a deprecated alias. The salary calculation request reads ytdAsrAmount directly rather than subtracting requestedGross from requestedYtdGross. Co-Authored-By: Claude Opus 5 (1M context) --- .../AdditionalSalaryRequest.graphql | 4 +- .../AdditionalSalaryRequest.test.tsx | 2 +- .../FormVersions/testUtils.tsx | 4 +- .../Helper/SplitCapSubContent.test.tsx | 4 +- .../RequestPage/RequestPage.test.tsx | 4 +- .../Shared/AdditionalSalaryRequestContext.tsx | 2 +- .../useAdditionalSalaryRequestForm.test.tsx | 6 +-- .../Shared/useAdditionalSalaryRequestForm.ts | 9 ++-- .../Shared/useSalaryCalculations.test.ts | 48 +++++++++---------- .../Shared/useSalaryCalculations.ts | 14 +++--- .../SharedComponents/CurrentRequest.test.tsx | 2 +- .../SharedComponents/StepList.test.tsx | 2 +- .../TotalSalaryRequested.test.tsx | 4 +- .../TotalSalaryRequested/TotalSalaryTable.tsx | 6 +-- .../RequestSummaryCard.test.tsx | 8 +++- .../RequestSummaryCard/RequestSummaryCard.tsx | 6 +-- .../SalaryCalculation.graphql | 1 + 17 files changed, 65 insertions(+), 61 deletions(-) diff --git a/src/components/HrTools/AdditionalSalaryRequest/AdditionalSalaryRequest.graphql b/src/components/HrTools/AdditionalSalaryRequest/AdditionalSalaryRequest.graphql index 53bada8b65..bd8180f141 100644 --- a/src/components/HrTools/AdditionalSalaryRequest/AdditionalSalaryRequest.graphql +++ b/src/components/HrTools/AdditionalSalaryRequest/AdditionalSalaryRequest.graphql @@ -42,13 +42,13 @@ fragment AdditionalSalaryRequestDetailsFields on AdditionalSalaryRequest { currentSalaryCap combinedCap staffAccountBalance - pendingAsrAmount + ytdAsrAmount grossAnnualSalary outstandingSalaryRequest } spouseCalculations { currentSalaryCap - pendingAsrAmount + ytdAsrAmount staffAccountBalance grossAnnualSalary } diff --git a/src/components/HrTools/AdditionalSalaryRequest/AdditionalSalaryRequest.test.tsx b/src/components/HrTools/AdditionalSalaryRequest/AdditionalSalaryRequest.test.tsx index d7f8f2547f..294606e0af 100644 --- a/src/components/HrTools/AdditionalSalaryRequest/AdditionalSalaryRequest.test.tsx +++ b/src/components/HrTools/AdditionalSalaryRequest/AdditionalSalaryRequest.test.tsx @@ -40,7 +40,7 @@ const mockRequest = { calculations: { currentSalaryCap: 50000, staffAccountBalance: 10000, - pendingAsrAmount: 0, + ytdAsrAmount: 0, grossAnnualSalary: 40000, outstandingSalaryRequest: false, }, diff --git a/src/components/HrTools/AdditionalSalaryRequest/FormVersions/testUtils.tsx b/src/components/HrTools/AdditionalSalaryRequest/FormVersions/testUtils.tsx index cbe43bfee3..9376cf9514 100644 --- a/src/components/HrTools/AdditionalSalaryRequest/FormVersions/testUtils.tsx +++ b/src/components/HrTools/AdditionalSalaryRequest/FormVersions/testUtils.tsx @@ -31,7 +31,7 @@ export const defaultMockContextValue = { calculations: { currentSalaryCap: 100000, staffAccountBalance: 40000, - pendingAsrAmount: 5000, + ytdAsrAmount: 5000, grossAnnualSalary: 40000, }, }, @@ -39,7 +39,7 @@ export const defaultMockContextValue = { calculations: { currentSalaryCap: 100000, staffAccountBalance: 40000, - pendingAsrAmount: 5000, + ytdAsrAmount: 5000, grossAnnualSalary: 40000, }, user: mockUser, diff --git a/src/components/HrTools/AdditionalSalaryRequest/RequestPage/Helper/SplitCapSubContent.test.tsx b/src/components/HrTools/AdditionalSalaryRequest/RequestPage/Helper/SplitCapSubContent.test.tsx index 66ac592280..17aedfd1c7 100644 --- a/src/components/HrTools/AdditionalSalaryRequest/RequestPage/Helper/SplitCapSubContent.test.tsx +++ b/src/components/HrTools/AdditionalSalaryRequest/RequestPage/Helper/SplitCapSubContent.test.tsx @@ -42,14 +42,14 @@ const setupContext = (opts: { calculations: { grossAnnualSalary: 50000, currentSalaryCap: 60000, - pendingAsrAmount: 0, + ytdAsrAmount: 0, }, spouseCalculations: spouseCap !== null ? { grossAnnualSalary: spouseGross, currentSalaryCap: spouseCap, - pendingAsrAmount: spousePending, + ytdAsrAmount: spousePending, } : null, }, diff --git a/src/components/HrTools/AdditionalSalaryRequest/RequestPage/RequestPage.test.tsx b/src/components/HrTools/AdditionalSalaryRequest/RequestPage/RequestPage.test.tsx index b1263e915f..d54714ce65 100644 --- a/src/components/HrTools/AdditionalSalaryRequest/RequestPage/RequestPage.test.tsx +++ b/src/components/HrTools/AdditionalSalaryRequest/RequestPage/RequestPage.test.tsx @@ -511,7 +511,7 @@ describe('RequestPage', () => { spouseCalculations: { grossAnnualSalary: 40000, currentSalaryCap: 500, - pendingAsrAmount: 600, + ytdAsrAmount: 600, }, progressiveApprovalTier: { id: 'tier-1' }, progressiveApprovalTierReason: @@ -723,7 +723,7 @@ describe('RequestPage', () => { currentSalaryCap: 10000, combinedCap: null, staffAccountBalance: 999999, - pendingAsrAmount: 0, + ytdAsrAmount: 0, }, progressiveApprovalTier: { id: 'tier-1' }, progressiveApprovalTierReason: diff --git a/src/components/HrTools/AdditionalSalaryRequest/Shared/AdditionalSalaryRequestContext.tsx b/src/components/HrTools/AdditionalSalaryRequest/Shared/AdditionalSalaryRequestContext.tsx index 89cbbf0e76..7f063b9ffe 100644 --- a/src/components/HrTools/AdditionalSalaryRequest/Shared/AdditionalSalaryRequestContext.tsx +++ b/src/components/HrTools/AdditionalSalaryRequest/Shared/AdditionalSalaryRequestContext.tsx @@ -107,7 +107,7 @@ export type AdditionalSalaryRequestType = { | 'currentSalaryCap' | 'combinedCap' | 'staffAccountBalance' - | 'pendingAsrAmount' + | 'ytdAsrAmount' | 'grossAnnualSalary' | 'outstandingSalaryRequest' >; diff --git a/src/components/HrTools/AdditionalSalaryRequest/Shared/useAdditionalSalaryRequestForm.test.tsx b/src/components/HrTools/AdditionalSalaryRequest/Shared/useAdditionalSalaryRequestForm.test.tsx index b578e8397c..9e51e38f6a 100644 --- a/src/components/HrTools/AdditionalSalaryRequest/Shared/useAdditionalSalaryRequestForm.test.tsx +++ b/src/components/HrTools/AdditionalSalaryRequest/Shared/useAdditionalSalaryRequestForm.test.tsx @@ -68,7 +68,7 @@ const defaultMockContextValue = { calculations: { currentSalaryCap: 100000, staffAccountBalance: 50000, - pendingAsrAmount: 0, + ytdAsrAmount: 0, grossAnnualSalary: 40000, }, }, @@ -163,7 +163,7 @@ const defaultGqlMocks: DeepPartial = { ProgressiveApprovalTierReasonEnum.OverUserCap, calculations: { currentSalaryCap: 100000, - pendingAsrAmount: 0, + ytdAsrAmount: 0, grossAnnualSalary: 40000, }, }, @@ -288,7 +288,7 @@ describe('useAdditionalSalaryRequestForm', () => { calculations: { currentSalaryCap: 50000, staffAccountBalance: 20000, - pendingAsrAmount: 0, + ytdAsrAmount: 0, }, }, }, diff --git a/src/components/HrTools/AdditionalSalaryRequest/Shared/useAdditionalSalaryRequestForm.ts b/src/components/HrTools/AdditionalSalaryRequest/Shared/useAdditionalSalaryRequestForm.ts index b1e26fce8d..a9202b930f 100644 --- a/src/components/HrTools/AdditionalSalaryRequest/Shared/useAdditionalSalaryRequestForm.ts +++ b/src/components/HrTools/AdditionalSalaryRequest/Shared/useAdditionalSalaryRequestForm.ts @@ -44,9 +44,8 @@ export const useAdditionalSalaryRequestForm = ( const individualCap = requestData?.latestAdditionalSalaryRequest?.calculations.currentSalaryCap ?? 0; - const pendingAsrAmount = - requestData?.latestAdditionalSalaryRequest?.calculations.pendingAsrAmount ?? - 0; + const ytdAsrAmount = + requestData?.latestAdditionalSalaryRequest?.calculations.ytdAsrAmount ?? 0; const grossAnnualSalary = requestData?.latestAdditionalSalaryRequest?.calculations .grossAnnualSalary ?? 0; @@ -187,7 +186,7 @@ export const useAdditionalSalaryRequestForm = ( : lastValidNonBackpayTotalRef.current; const amountRequestedYtd = - grossAnnualSalary + pendingAsrAmount + stableNonBackpayTotal; + grossAnnualSalary + ytdAsrAmount + stableNonBackpayTotal; const exceedsCap = amountRequestedYtd > individualCap; if (exceedsCap) { @@ -209,7 +208,7 @@ export const useAdditionalSalaryRequestForm = ( t, primaryAccountBalance, individualCap, - pendingAsrAmount, + ytdAsrAmount, grossAnnualSalary, locale, requestData, diff --git a/src/components/HrTools/AdditionalSalaryRequest/Shared/useSalaryCalculations.test.ts b/src/components/HrTools/AdditionalSalaryRequest/Shared/useSalaryCalculations.test.ts index c61a5a0225..560ecc14d7 100644 --- a/src/components/HrTools/AdditionalSalaryRequest/Shared/useSalaryCalculations.test.ts +++ b/src/components/HrTools/AdditionalSalaryRequest/Shared/useSalaryCalculations.test.ts @@ -225,7 +225,7 @@ describe('useSalaryCalculations', () => { calculations: { grossAnnualSalary: 1000, currentSalaryCap: 5000, - pendingAsrAmount: 1000, + ytdAsrAmount: 1000, }, }, }, @@ -247,7 +247,7 @@ describe('useSalaryCalculations', () => { ); expect(result.current.total).toBe(2000); - // requestedAnnualSalary = grossAnnualSalary + additionalSalaryReceivedThisYear + total + // requestedAnnualSalary = grossAnnualSalary + additionalSalaryRequestedThisYear + total // = 1000 + 1000 + 2000 = 4000 expect(result.current.requestedAnnualSalary).toBe(4000); // requestedAnnualSalary (4000) <= individualCap (5000) @@ -263,7 +263,7 @@ describe('useSalaryCalculations', () => { calculations: { grossAnnualSalary: 50000, currentSalaryCap: 10000, - pendingAsrAmount: 10000, + ytdAsrAmount: 10000, }, }, }, @@ -300,7 +300,7 @@ describe('useSalaryCalculations', () => { calculations: { grossAnnualSalary: 72000, currentSalaryCap: 80000, - pendingAsrAmount: 0, + ytdAsrAmount: 0, }, }, }, @@ -329,7 +329,7 @@ describe('useSalaryCalculations', () => { calculations: { grossAnnualSalary: 50000, currentSalaryCap: 60000, - pendingAsrAmount: 0, + ytdAsrAmount: 0, }, }, }, @@ -356,7 +356,7 @@ describe('useSalaryCalculations', () => { // Cases 1-4: Staff Member under cap — nothing triggers regardless of spouse status const setupUnderCap = (spouseCalculations: { currentSalaryCap: number; - pendingAsrAmount: number; + ytdAsrAmount: number; }) => { mockUseAdditionalSalaryRequest.mockReturnValue({ traditional403bPercentage: 0.12, @@ -378,7 +378,7 @@ describe('useSalaryCalculations', () => { }; it('Staff Member under cap, spouse has no pending ASR', () => { - setupUnderCap({ currentSalaryCap: 50000, pendingAsrAmount: 0 }); + setupUnderCap({ currentSalaryCap: 50000, ytdAsrAmount: 0 }); const values: CompleteFormValues = { ...baseValues, @@ -397,7 +397,7 @@ describe('useSalaryCalculations', () => { }); it('Staff Member under cap, spouse has ASR under their cap', () => { - setupUnderCap({ currentSalaryCap: 50000, pendingAsrAmount: 5000 }); + setupUnderCap({ currentSalaryCap: 50000, ytdAsrAmount: 5000 }); const values: CompleteFormValues = { ...baseValues, @@ -416,7 +416,7 @@ describe('useSalaryCalculations', () => { }); it('Staff Member under cap, spouse is over their cap', () => { - setupUnderCap({ currentSalaryCap: 50000, pendingAsrAmount: 15000 }); + setupUnderCap({ currentSalaryCap: 50000, ytdAsrAmount: 15000 }); const values: CompleteFormValues = { ...baseValues, @@ -435,7 +435,7 @@ describe('useSalaryCalculations', () => { }); it('Staff Member under cap, spouse is at their cap', () => { - setupUnderCap({ currentSalaryCap: 40003, pendingAsrAmount: 0 }); + setupUnderCap({ currentSalaryCap: 40003, ytdAsrAmount: 0 }); const values: CompleteFormValues = { ...baseValues, @@ -458,7 +458,7 @@ describe('useSalaryCalculations', () => { const setupOverCap = ( spouseCalculations: { currentSalaryCap: number; - pendingAsrAmount: number; + ytdAsrAmount: number; }, progressiveApprovalTier: unknown = null, ) => { @@ -483,7 +483,7 @@ describe('useSalaryCalculations', () => { }; it('Staff Member over cap, spouse has no pending ASR — splitAsr', () => { - setupOverCap({ currentSalaryCap: 50000, pendingAsrAmount: 0 }); + setupOverCap({ currentSalaryCap: 50000, ytdAsrAmount: 0 }); const values: CompleteFormValues = { ...baseValues, @@ -503,7 +503,7 @@ describe('useSalaryCalculations', () => { }); it('Staff Member over cap, spouse has ASR under their cap — splitAsr', () => { - setupOverCap({ currentSalaryCap: 50000, pendingAsrAmount: 5000 }); + setupOverCap({ currentSalaryCap: 50000, ytdAsrAmount: 5000 }); const values: CompleteFormValues = { ...baseValues, @@ -524,7 +524,7 @@ describe('useSalaryCalculations', () => { it('Staff Member over cap, spouse is over their cap — additionalApproval', () => { setupOverCap( - { currentSalaryCap: 50000, pendingAsrAmount: 15000 }, + { currentSalaryCap: 50000, ytdAsrAmount: 15000 }, { id: 'tier-1' }, ); @@ -547,7 +547,7 @@ describe('useSalaryCalculations', () => { it('Staff Member over cap, spouse is at their cap — additionalApproval', () => { setupOverCap( - { currentSalaryCap: 40003, pendingAsrAmount: 0 }, + { currentSalaryCap: 40003, ytdAsrAmount: 0 }, { id: 'tier-1' }, ); @@ -570,7 +570,7 @@ describe('useSalaryCalculations', () => { it('Staff Member over cap, spouse exactly $5 below cap — treated as at cap', () => { const currentSalaryCap = 40000 + AT_CAP_TOLERANCE; - setupOverCap({ currentSalaryCap, pendingAsrAmount: 0 }, { id: 'tier-1' }); + setupOverCap({ currentSalaryCap, ytdAsrAmount: 0 }, { id: 'tier-1' }); const values: CompleteFormValues = { ...baseValues, @@ -591,7 +591,7 @@ describe('useSalaryCalculations', () => { it('Staff Member over cap, spouse $6 below cap — not at cap, splitAsr', () => { const currentSalaryCap = 40000 + AT_CAP_TOLERANCE + 1; - setupOverCap({ currentSalaryCap, pendingAsrAmount: 0 }); + setupOverCap({ currentSalaryCap, ytdAsrAmount: 0 }); const values: CompleteFormValues = { ...baseValues, @@ -616,7 +616,7 @@ describe('useSalaryCalculations', () => { const setupAtCap = ( spouseCalculations: { currentSalaryCap: number; - pendingAsrAmount: number; + ytdAsrAmount: number; }, progressiveApprovalTier: unknown = null, ) => { @@ -641,7 +641,7 @@ describe('useSalaryCalculations', () => { }; it('Staff Member at cap, spouse has no pending ASR', () => { - setupAtCap({ currentSalaryCap: 50000, pendingAsrAmount: 0 }); + setupAtCap({ currentSalaryCap: 50000, ytdAsrAmount: 0 }); const values: CompleteFormValues = { ...baseValues, @@ -660,7 +660,7 @@ describe('useSalaryCalculations', () => { }); it('Staff Member at cap, spouse has ASR under their cap', () => { - setupAtCap({ currentSalaryCap: 50000, pendingAsrAmount: 5000 }); + setupAtCap({ currentSalaryCap: 50000, ytdAsrAmount: 5000 }); const values: CompleteFormValues = { ...baseValues, @@ -680,7 +680,7 @@ describe('useSalaryCalculations', () => { it('Staff Member at cap, spouse is over their cap', () => { setupAtCap( - { currentSalaryCap: 50000, pendingAsrAmount: 15000 }, + { currentSalaryCap: 50000, ytdAsrAmount: 15000 }, { id: 'tier-1' }, ); @@ -702,7 +702,7 @@ describe('useSalaryCalculations', () => { }); it('Staff Member at cap, spouse is at their cap', () => { - setupAtCap({ currentSalaryCap: 40003, pendingAsrAmount: 0 }); + setupAtCap({ currentSalaryCap: 40003, ytdAsrAmount: 0 }); const values: CompleteFormValues = { ...baseValues, @@ -809,7 +809,7 @@ describe('useSalaryCalculations', () => { spouseCalculations: { grossAnnualSalary: 40000, currentSalaryCap: 50000, - pendingAsrAmount: 2000, + ytdAsrAmount: 2000, }, }, }, @@ -839,7 +839,7 @@ describe('useSalaryCalculations', () => { spouseCalculations: { grossAnnualSalary: 40000, currentSalaryCap: 50000, - pendingAsrAmount: 15000, + ytdAsrAmount: 15000, }, }, }, diff --git a/src/components/HrTools/AdditionalSalaryRequest/Shared/useSalaryCalculations.ts b/src/components/HrTools/AdditionalSalaryRequest/Shared/useSalaryCalculations.ts index 6c84a1af96..c22be51b97 100644 --- a/src/components/HrTools/AdditionalSalaryRequest/Shared/useSalaryCalculations.ts +++ b/src/components/HrTools/AdditionalSalaryRequest/Shared/useSalaryCalculations.ts @@ -16,7 +16,7 @@ export interface SalaryCalculations { calculatedRothDeduction: number; totalDeduction: number; netSalary: number; - additionalSalaryReceivedThisYear: number; + additionalSalaryRequestedThisYear: number; /** * User's total salary requested this year including their current gross salary, any * outstanding ASRs, and this ASR. This is the amount we compare against their individual cap. @@ -110,16 +110,16 @@ export const useSalaryCalculations = ({ const netSalary = total - totalDeduction; // Annual salary calculations - const additionalSalaryReceivedThisYear = - requestData?.latestAdditionalSalaryRequest?.calculations - ?.pendingAsrAmount ?? 0; + const additionalSalaryRequestedThisYear = + requestData?.latestAdditionalSalaryRequest?.calculations?.ytdAsrAmount ?? + 0; const requestedAnnualSalary = - grossAnnualSalary + additionalSalaryReceivedThisYear + nonBackpayTotal; + grossAnnualSalary + additionalSalaryRequestedThisYear + nonBackpayTotal; // Spouse annual salary calculations const spouseTotalThisYear = requestData?.latestAdditionalSalaryRequest?.spouseCalculations - ?.pendingAsrAmount ?? 0; + ?.ytdAsrAmount ?? 0; const spouseRequestedAnnualSalary = spouseGrossAnnualSalary + spouseTotalThisYear; @@ -175,7 +175,7 @@ export const useSalaryCalculations = ({ totalDeduction, netSalary, grossAnnualSalary, - additionalSalaryReceivedThisYear, + additionalSalaryRequestedThisYear, requestedAnnualSalary, exceedsCap, splitAsr, diff --git a/src/components/HrTools/AdditionalSalaryRequest/SharedComponents/CurrentRequest.test.tsx b/src/components/HrTools/AdditionalSalaryRequest/SharedComponents/CurrentRequest.test.tsx index a7f09b0164..679200b48d 100644 --- a/src/components/HrTools/AdditionalSalaryRequest/SharedComponents/CurrentRequest.test.tsx +++ b/src/components/HrTools/AdditionalSalaryRequest/SharedComponents/CurrentRequest.test.tsx @@ -56,7 +56,7 @@ const mockRequest: RequestType = { calculations: { currentSalaryCap: 50000, staffAccountBalance: 10000, - pendingAsrAmount: 0, + ytdAsrAmount: 0, grossAnnualSalary: 40000, outstandingSalaryRequest: false, }, diff --git a/src/components/HrTools/AdditionalSalaryRequest/SharedComponents/StepList.test.tsx b/src/components/HrTools/AdditionalSalaryRequest/SharedComponents/StepList.test.tsx index 1f7c1c966a..fb4cf6b231 100644 --- a/src/components/HrTools/AdditionalSalaryRequest/SharedComponents/StepList.test.tsx +++ b/src/components/HrTools/AdditionalSalaryRequest/SharedComponents/StepList.test.tsx @@ -356,7 +356,7 @@ describe('StepList', () => { }, spouseCalculations: { currentSalaryCap: 500, - pendingAsrAmount: 600, + ytdAsrAmount: 600, }, }, }, diff --git a/src/components/HrTools/AdditionalSalaryRequest/SubmitModalAccordions/TotalSalaryRequested/TotalSalaryRequested.test.tsx b/src/components/HrTools/AdditionalSalaryRequest/SubmitModalAccordions/TotalSalaryRequested/TotalSalaryRequested.test.tsx index 0a5ef41fc9..f0dc314f81 100644 --- a/src/components/HrTools/AdditionalSalaryRequest/SubmitModalAccordions/TotalSalaryRequested/TotalSalaryRequested.test.tsx +++ b/src/components/HrTools/AdditionalSalaryRequest/SubmitModalAccordions/TotalSalaryRequested/TotalSalaryRequested.test.tsx @@ -31,14 +31,14 @@ const defaultMockContextValue = { traditional403bContribution: 0.12, calculations: { currentSalaryCap: 100000, - pendingAsrAmount: 5000, + ytdAsrAmount: 5000, grossAnnualSalary: 50000, }, }, }, calculations: { currentSalaryCap: 100000, - pendingAsrAmount: 5000, + ytdAsrAmount: 5000, grossAnnualSalary: 50000, }, }; diff --git a/src/components/HrTools/AdditionalSalaryRequest/SubmitModalAccordions/TotalSalaryRequested/TotalSalaryTable.tsx b/src/components/HrTools/AdditionalSalaryRequest/SubmitModalAccordions/TotalSalaryRequested/TotalSalaryTable.tsx index f792fb404e..2745226fce 100644 --- a/src/components/HrTools/AdditionalSalaryRequest/SubmitModalAccordions/TotalSalaryRequested/TotalSalaryTable.tsx +++ b/src/components/HrTools/AdditionalSalaryRequest/SubmitModalAccordions/TotalSalaryRequested/TotalSalaryTable.tsx @@ -57,7 +57,7 @@ export const TotalSalaryTable: React.FC = () => { const { nonBackpayTotal, requestedAnnualSalary, - additionalSalaryReceivedThisYear, + additionalSalaryRequestedThisYear, grossAnnualSalary, } = useSalaryCalculations({ values, @@ -80,7 +80,7 @@ export const TotalSalaryTable: React.FC = () => { id: 'additionalReceived', label: t('Additional Salary Previously Requested This Year'), description: backpayExplanation, - value: additionalSalaryReceivedThisYear, + value: additionalSalaryRequestedThisYear, }, { id: 'additionalRequested', @@ -93,7 +93,7 @@ export const TotalSalaryTable: React.FC = () => { t, individualCap, grossAnnualSalary, - additionalSalaryReceivedThisYear, + additionalSalaryRequestedThisYear, nonBackpayTotal, outstandingSalaryRequest, salaryRequestTooltip, diff --git a/src/components/HrTools/SalaryCalculator/SalaryCalculation/RequestSummaryCard/RequestSummaryCard.test.tsx b/src/components/HrTools/SalaryCalculator/SalaryCalculation/RequestSummaryCard/RequestSummaryCard.test.tsx index b58ea224ec..df91856efe 100644 --- a/src/components/HrTools/SalaryCalculator/SalaryCalculation/RequestSummaryCard/RequestSummaryCard.test.tsx +++ b/src/components/HrTools/SalaryCalculator/SalaryCalculation/RequestSummaryCard/RequestSummaryCard.test.tsx @@ -21,6 +21,7 @@ const defaultSalaryMock: DeepPartial = contributing403bAmount: 10003, requestedGross: 10004, requestedYtdGross: 10005, + ytdAsrAmount: 1, effectiveCap: 10006, }, spouseCalculations: { @@ -28,6 +29,7 @@ const defaultSalaryMock: DeepPartial = contributing403bAmount: 20003, requestedGross: 20004, requestedYtdGross: 20005, + ytdAsrAmount: 1, effectiveCap: 20006, }, }; @@ -236,7 +238,11 @@ This may affect your selected effective date.', , ); diff --git a/src/components/HrTools/SalaryCalculator/SalaryCalculation/RequestSummaryCard/RequestSummaryCard.tsx b/src/components/HrTools/SalaryCalculator/SalaryCalculation/RequestSummaryCard/RequestSummaryCard.tsx index ed3ffc3029..1bd9eae2b0 100644 --- a/src/components/HrTools/SalaryCalculator/SalaryCalculation/RequestSummaryCard/RequestSummaryCard.tsx +++ b/src/components/HrTools/SalaryCalculator/SalaryCalculation/RequestSummaryCard/RequestSummaryCard.tsx @@ -83,10 +83,8 @@ export const RequestSummaryCard: React.FC = () => { const combined403b = (calcs?.contributing403bAmount ?? 0) + (spouseCalcs?.contributing403bAmount ?? 0); - const asrAmount = - (calcs?.requestedYtdGross ?? 0) - (calcs?.requestedGross ?? 0); - const spouseAsrAmount = - (spouseCalcs?.requestedYtdGross ?? 0) - (spouseCalcs?.requestedGross ?? 0); + const asrAmount = calcs?.ytdAsrAmount ?? 0; + const spouseAsrAmount = spouseCalcs?.ytdAsrAmount ?? 0; const categories: Category[] = [ { diff --git a/src/components/HrTools/SalaryCalculator/SalaryCalculatorContext/SalaryCalculation.graphql b/src/components/HrTools/SalaryCalculator/SalaryCalculatorContext/SalaryCalculation.graphql index 938af05412..72d1086212 100644 --- a/src/components/HrTools/SalaryCalculator/SalaryCalculatorContext/SalaryCalculation.graphql +++ b/src/components/HrTools/SalaryCalculator/SalaryCalculatorContext/SalaryCalculation.graphql @@ -57,6 +57,7 @@ fragment CalculationFields on SalaryRequestCalculations { requestedWithSeca requestedGross requestedYtdGross + ytdAsrAmount requestedAboveMinimum requestedMha requestedAboveMha From c590f3689b4e07c04db8783dc519d233b984e323 Mon Sep 17 00:00:00 2001 From: Caleb Cox Date: Fri, 4 Sep 2026 13:06:58 -0400 Subject: [PATCH 3/3] Add a tooltip explaining how much is unpaid --- .../TotalSalaryRequested.test.tsx | 37 ++++++++++++ .../TotalSalaryRequested/TotalSalaryTable.tsx | 51 ++++++++-------- .../RequestSummaryCard/Distribution.tsx | 1 + .../RequestSummaryCard.test.tsx | 59 +++++++++++++++++++ .../RequestSummaryCard/RequestSummaryCard.tsx | 25 ++++++-- .../HrTools/Shared/YtdAsrTooltip.tsx | 39 ++++++++++++ 6 files changed, 183 insertions(+), 29 deletions(-) create mode 100644 src/components/HrTools/Shared/YtdAsrTooltip.tsx diff --git a/src/components/HrTools/AdditionalSalaryRequest/SubmitModalAccordions/TotalSalaryRequested/TotalSalaryRequested.test.tsx b/src/components/HrTools/AdditionalSalaryRequest/SubmitModalAccordions/TotalSalaryRequested/TotalSalaryRequested.test.tsx index f0dc314f81..90988dc49e 100644 --- a/src/components/HrTools/AdditionalSalaryRequest/SubmitModalAccordions/TotalSalaryRequested/TotalSalaryRequested.test.tsx +++ b/src/components/HrTools/AdditionalSalaryRequest/SubmitModalAccordions/TotalSalaryRequested/TotalSalaryRequested.test.tsx @@ -123,6 +123,43 @@ describe('TotalSalaryRequested', () => { ).not.toBeInTheDocument(); }); + it('explains the pending and approved ASRs behind the amount', async () => { + const { getAllByLabelText, getByRole } = renderComponent(); + + userEvent.click(getByRole('button', { name: 'Expand salary details' })); + + await waitFor(() => + expect( + getAllByLabelText( + 'Includes $5,000.00 of pending and approved requests this year', + ), + ).toHaveLength(1), + ); + }); + + it('omits the ASR explanation when there are none this year', async () => { + const { getByRole, queryByTestId } = renderComponent({ + contextOverrides: { + requestData: { + latestAdditionalSalaryRequest: { + ...defaultMockContextValue.requestData + .latestAdditionalSalaryRequest, + calculations: { + ...defaultMockContextValue.requestData + .latestAdditionalSalaryRequest.calculations, + ytdAsrAmount: 0, + }, + }, + }, + }, + }); + + userEvent.click(getByRole('button', { name: 'Expand salary details' })); + + await waitFor(() => expect(getByRole('table')).toBeInTheDocument()); + expect(queryByTestId('YtdAsrTooltip')).not.toBeInTheDocument(); + }); + it('renders accordion with title, table, and summary items', async () => { const { getAllByText, getByText, queryByTestId, getByRole } = renderComponent(); diff --git a/src/components/HrTools/AdditionalSalaryRequest/SubmitModalAccordions/TotalSalaryRequested/TotalSalaryTable.tsx b/src/components/HrTools/AdditionalSalaryRequest/SubmitModalAccordions/TotalSalaryRequested/TotalSalaryTable.tsx index 2745226fce..d425bd250b 100644 --- a/src/components/HrTools/AdditionalSalaryRequest/SubmitModalAccordions/TotalSalaryRequested/TotalSalaryTable.tsx +++ b/src/components/HrTools/AdditionalSalaryRequest/SubmitModalAccordions/TotalSalaryRequested/TotalSalaryTable.tsx @@ -13,6 +13,7 @@ import { import { useFormikContext } from 'formik'; import { DateTime } from 'luxon'; import { useTranslation } from 'react-i18next'; +import { YtdAsrTooltip } from 'src/components/HrTools/Shared/YtdAsrTooltip'; import { useLocale } from 'src/hooks/useLocale'; import { currencyFormat } from 'src/lib/intlFormat'; import { CompleteFormValues } from '../../AdditionalSalaryRequest'; @@ -81,6 +82,7 @@ export const TotalSalaryTable: React.FC = () => { label: t('Additional Salary Previously Requested This Year'), description: backpayExplanation, value: additionalSalaryRequestedThisYear, + ytdAsrAmount: additionalSalaryRequestedThisYear, }, { id: 'additionalRequested', @@ -114,30 +116,33 @@ export const TotalSalaryTable: React.FC = () => { - {summaryItems.map(({ id, label, description, tooltip, value }) => ( - - - - {label} - {tooltip && ( - - - - )} - - {description && ( - - {description} + {summaryItems.map( + ({ id, label, description, tooltip, value, ytdAsrAmount }) => ( + + + + {label} + {tooltip && ( + + + + )} - )} - - - {currencyFormat(value, currency, locale, { - showTrailingZeros: true, - })} - - - ))} + {description && ( + + {description} + + )} + + + {currencyFormat(value, currency, locale, { + showTrailingZeros: true, + })} + + + + ), + )} = ({ invalid, }) => ( ({ border: invalid ? `2px solid ${theme.palette.error.main}` : undefined, })} diff --git a/src/components/HrTools/SalaryCalculator/SalaryCalculation/RequestSummaryCard/RequestSummaryCard.test.tsx b/src/components/HrTools/SalaryCalculator/SalaryCalculation/RequestSummaryCard/RequestSummaryCard.test.tsx index df91856efe..9d82491973 100644 --- a/src/components/HrTools/SalaryCalculator/SalaryCalculation/RequestSummaryCard/RequestSummaryCard.test.tsx +++ b/src/components/HrTools/SalaryCalculator/SalaryCalculation/RequestSummaryCard/RequestSummaryCard.test.tsx @@ -1,4 +1,5 @@ import { render, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { merge } from 'lodash'; import { DeepPartial } from 'ts-essentials'; import { @@ -204,6 +205,31 @@ This may affect your selected effective date.', ); }); + it('splits the distribution bar between the salary components and the ASRs', async () => { + const { findByTestId } = render( + , + ); + + const segments = Array.from((await findByTestId('Distribution')).children); + expect(segments[0]).toHaveStyle('width: 50%'); + expect(segments[1]).toHaveStyle('width: 25%'); + expect(segments[2]).toHaveStyle('width: 12.5%'); + expect(segments[3]).toHaveStyle('width: 12.5%'); + }); + describe('table', () => { it('renders table headers, row headers, and cells', async () => { const { getByRole } = render(); @@ -262,6 +288,39 @@ This may affect your selected effective date.', ); }); + it('explains the pending and approved ASRs behind the amount', async () => { + const { findAllByTestId, findByRole } = render( + , + ); + + const [icon] = await findAllByTestId('YtdAsrTooltip'); + expect(icon.closest('tr')).toHaveTextContent( + 'Additional Salary Requested This Year', + ); + + userEvent.hover(icon); + expect(await findByRole('tooltip')).toHaveTextContent( + 'Includes $5,000.00 of pending and approved requests this year', + ); + }); + + it('omits the ASR explanation when there are none this year', async () => { + const { findByRole, queryByTestId } = render( + , + ); + + await findByRole('table'); + expect(queryByTestId('YtdAsrTooltip')).not.toBeInTheDocument(); + }); + it('shows SECA opt-out text', async () => { const { findByText, getByText } = render( { !!progressiveApprovalTier && progressiveApprovalTier?.tier !== ProgressiveApprovalTierEnum.DivisionHead; const { - combinedGross, combinedYtdGross, combinedEffectiveCap: combinedCap, overCapPerson, @@ -78,6 +78,9 @@ export const RequestSummaryCard: React.FC = () => { const spouseCalcs = calculation?.spouseCalculations; const hasSpouse = !!hcmSpouse && !!spouseCalcs; + const combinedSalary = + (calculation?.salary ?? 0) + + (hasSpouse ? (calculation?.spouseSalary ?? 0) : 0); const combinedSeca = (calcs?.requestedSeca ?? 0) + (spouseCalcs?.requestedSeca ?? 0); const combined403b = @@ -89,9 +92,14 @@ export const RequestSummaryCard: React.FC = () => { const categories: Category[] = [ { label: t('Requested Salary (includes MHA)'), - amount: combinedGross, + amount: combinedSalary, color: theme.palette.yellow.main, }, + { + label: t('Additional Salary Requested This Year'), + amount: asrAmount + spouseAsrAmount, + color: theme.palette.orange.main, + }, { label: t('SECA and Related Federal Taxes'), amount: combinedSeca, @@ -309,9 +317,15 @@ export const RequestSummaryCard: React.FC = () => { })} - {formatCurrency(asrAmount)} + + {formatCurrency(asrAmount)} + + {hasSpouse && ( - {formatCurrency(spouseAsrAmount)} + + {formatCurrency(spouseAsrAmount)} + + )} @@ -320,8 +334,7 @@ export const RequestSummaryCard: React.FC = () => { {t('Total Gross Salary This Year')} diff --git a/src/components/HrTools/Shared/YtdAsrTooltip.tsx b/src/components/HrTools/Shared/YtdAsrTooltip.tsx new file mode 100644 index 0000000000..6bdc12b44e --- /dev/null +++ b/src/components/HrTools/Shared/YtdAsrTooltip.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import InfoIcon from '@mui/icons-material/Info'; +import { Tooltip, styled } from '@mui/material'; +import { useTranslation } from 'react-i18next'; +import { useFormatters } from './useFormatters'; + +const StyledInfoIcon = styled(InfoIcon)(({ theme }) => ({ + marginLeft: theme.spacing(0.5), + verticalAlign: 'middle', + cursor: 'pointer', + color: theme.palette.mpdxGrayDark.main, + fontSize: '1rem', +})); + +interface YtdAsrTooltipProps { + ytdAsrAmount: number; +} + +export const YtdAsrTooltip: React.FC = ({ + ytdAsrAmount, +}) => { + const { t } = useTranslation(); + const { formatCurrency } = useFormatters(); + + if (ytdAsrAmount <= 0) { + return null; + } + + return ( + + + + ); +};