From e0232dc345c436fd136f3b096a48355732ebf5dc Mon Sep 17 00:00:00 2001 From: Katelyn Grimes Date: Wed, 12 Aug 2026 11:43:16 -0400 Subject: [PATCH 1/9] Add geographic location accordion to account preferences --- .../settings/preferences.page.tsx | 17 +- .../preferences/GetAccountPreferences.graphql | 1 + .../GeographicLocationAccordion.test.tsx | 201 ++++++++++++++++++ .../GeographicLocationAccordion.tsx | 134 ++++++++++++ .../Shared/Forms/Accordions/AccordionEnum.ts | 1 + 5 files changed, 353 insertions(+), 1 deletion(-) create mode 100644 src/components/Settings/preferences/accordions/GeographicLocationAccordion/GeographicLocationAccordion.test.tsx create mode 100644 src/components/Settings/preferences/accordions/GeographicLocationAccordion/GeographicLocationAccordion.tsx diff --git a/pages/accountLists/[accountListId]/settings/preferences.page.tsx b/pages/accountLists/[accountListId]/settings/preferences.page.tsx index 974d80faf6..87f25d8edf 100644 --- a/pages/accountLists/[accountListId]/settings/preferences.page.tsx +++ b/pages/accountLists/[accountListId]/settings/preferences.page.tsx @@ -16,6 +16,7 @@ import { CurrencyAccordion } from 'src/components/Settings/preferences/accordion import { DefaultAccountAccordion } from 'src/components/Settings/preferences/accordions/DefaultAccountAccordion/DefaultAccountAccordion'; import { EarlyAdopterAccordion } from 'src/components/Settings/preferences/accordions/EarlyAdopterAccordion/EarlyAdopterAccordion'; import { ExportAllDataAccordion } from 'src/components/Settings/preferences/accordions/ExportAllDataAccordion/ExportAllDataAccordion'; +import { GeographicLocationAccordion } from 'src/components/Settings/preferences/accordions/GeographicLocationAccordion/GeographicLocationAccordion'; import { HomeCountryAccordion } from 'src/components/Settings/preferences/accordions/HomeCountryAccordion/HomeCountryAccordion'; import { HourToSendNotificationsAccordion } from 'src/components/Settings/preferences/accordions/HourToSendNotificationsAccordion/HourToSendNotificationsAccordion'; import { LanguageAccordion } from 'src/components/Settings/preferences/accordions/LanguageAccordion/LanguageAccordion'; @@ -51,6 +52,7 @@ const Preferences: React.FC = () => { const setupAccordions = [ PreferenceAccordion.Locale, PreferenceAccordion.MonthlyGoal, + PreferenceAccordion.GeographicLocation, PreferenceAccordion.HomeCountry, ]; const [setup, setSetup] = useState(0); @@ -139,6 +141,8 @@ const Preferences: React.FC = () => { case 1: return t('Great progress comes from great goals!'); case 2: + return t('Are you within 50 miles of a major city?'); + case 3: return t('What country are you in?'); default: return ''; @@ -254,6 +258,17 @@ const Preferences: React.FC = () => { disabled={onSetupTour && setup !== 1} handleSetupChange={handleSetupChange} /> + { } accountListId={accountListId} countries={countries} - disabled={onSetupTour && setup !== 2} + disabled={onSetupTour && setup !== 3} handleSetupChange={handleSetupChange} /> ({ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + ...jest.requireActual('notistack'), + useSnackbar: () => { + return { + enqueueSnackbar: mockEnqueue, + }; + }, +})); + +const handleAccordionChange = jest.fn(); +const handleSetupChange = jest.fn(); +const mutationSpy = jest.fn(); + +const geographicConstants = [ + { location: 'Chicago, IL', percentageMultiplier: 0.1 }, + { location: 'Los Angeles, CA', percentageMultiplier: 0.15 }, +]; + +interface ComponentsProps { + geographicLocation: string; + expandedAccordion: PreferenceAccordion | null; +} + +const Components: React.FC = ({ + geographicLocation, + expandedAccordion, +}) => ( + + + + + + + + + +); + +const errorMocks: MockedResponse[] = [ + { + request: { + query: GoalCalculatorConstantsDocument, + variables: { year: null }, + }, + result: { + data: { + constant: { + mpdGoalBenefitsConstants: [], + mpdGoalGeographicConstants: [], + mpdGoalMiscConstants: [], + }, + }, + }, + }, + { + request: { + query: UpdateAccountPreferencesDocument, + }, + error: { name: 'error', message: 'Error loading data. Try again.' }, + }, +]; + +describe('GeographicLocationAccordion', () => { + afterEach(() => { + mutationSpy.mockClear(); + }); + + it('should render accordion closed', () => { + const { getByText, queryByRole } = render( + , + ); + + expect(getByText(label)).toBeInTheDocument(); + expect(queryByRole('combobox')).not.toBeInTheDocument(); + }); + + it('allows saving a blank value', async () => { + const value = ''; + + const { getByRole } = render( + , + ); + + const button = getByRole('button', { name: 'Save' }); + + await waitFor(() => { + expect(button).not.toBeDisabled(); + }); + }); + + it('changes and saves the input', async () => { + const { getByRole, getByText } = render( + , + ); + const button = getByRole('button', { name: 'Save' }); + const input = getByRole('combobox'); + + await waitFor(() => expect(input).toHaveValue('Chicago, IL')); + + userEvent.click(input); + userEvent.click(getByText('Los Angeles, CA')); + userEvent.click(button); + + await waitFor(() => { + expect(mutationSpy.mock.lastCall).toMatchObject([ + { + operation: { + operationName: 'UpdateAccountPreferences', + variables: { + input: { + id: accountListId, + attributes: { + id: accountListId, + settings: { + geographicLocation: 'Los Angeles, CA', + }, + }, + }, + }, + }, + }, + ]); + }); + }); + + it('Should render the error state', async () => { + const { getByRole } = render( + + + + + + + + + , + ); + const button = getByRole('button', { name: 'Save' }); + + userEvent.click(button); + + await waitFor(() => { + expect(mockEnqueue).toHaveBeenCalledWith('Saving failed.', { + variant: 'error', + }); + }); + }); +}); diff --git a/src/components/Settings/preferences/accordions/GeographicLocationAccordion/GeographicLocationAccordion.tsx b/src/components/Settings/preferences/accordions/GeographicLocationAccordion/GeographicLocationAccordion.tsx new file mode 100644 index 0000000000..880a531b1c --- /dev/null +++ b/src/components/Settings/preferences/accordions/GeographicLocationAccordion/GeographicLocationAccordion.tsx @@ -0,0 +1,134 @@ +import { ReactElement, useMemo } from 'react'; +import { Autocomplete, TextField } from '@mui/material'; +import { Formik } from 'formik'; +import { useSnackbar } from 'notistack'; +import { useTranslation } from 'react-i18next'; +import * as yup from 'yup'; +import { PreferenceAccordion } from 'src/components/Shared/Forms/Accordions/AccordionEnum'; +import { AccordionItem } from 'src/components/Shared/Forms/Accordions/AccordionItem'; +import { FieldWrapper } from 'src/components/Shared/Forms/FieldWrapper'; +import { FormWrapper } from 'src/components/Shared/Forms/FormWrapper'; +import { AccountListSettingsInput } from 'src/graphql/types.generated'; +import { useGoalCalculatorConstants } from 'src/hooks/useGoalCalculatorConstants'; +import { AccordionProps } from '../../../accordionHelper'; +import { useUpdateAccountPreferencesMutation } from '../UpdateAccountPreferences.generated'; + +const accountPreferencesSchema: yup.ObjectSchema< + Pick +> = yup.object({ + geographicLocation: yup.string().nullable(), +}); + +interface GeographicLocationAccordionProps + extends AccordionProps { + geographicLocation: string; + accountListId: string; + disabled?: boolean; + handleSetupChange: () => Promise; +} + +export const GeographicLocationAccordion: React.FC< + GeographicLocationAccordionProps +> = ({ + handleAccordionChange, + expandedAccordion, + geographicLocation, + accountListId, + disabled, + handleSetupChange, +}) => { + const { t } = useTranslation(); + const { enqueueSnackbar } = useSnackbar(); + const [updateAccountPreferences] = useUpdateAccountPreferencesMutation(); + const label = t('Geographic Location'); + + const { goalGeographicConstantMap } = useGoalCalculatorConstants(); + const locations = useMemo( + () => Array.from(goalGeographicConstantMap.keys()), + [goalGeographicConstantMap], + ); + + const onSubmit = async ( + attributes: Pick, + ) => { + await updateAccountPreferences({ + variables: { + input: { + id: accountListId, + attributes: { + id: accountListId, + settings: { geographicLocation: attributes.geographicLocation }, + }, + }, + }, + onCompleted: () => { + enqueueSnackbar(t('Saved successfully.'), { + variant: 'success', + }); + handleAccordionChange(null); + }, + onError: () => { + enqueueSnackbar(t('Saving failed.'), { + variant: 'error', + }); + }, + }); + handleSetupChange(); + }; + + return ( + + + {({ + values: { geographicLocation }, + handleSubmit, + isSubmitting, + isValid, + setFieldValue, + }): ReactElement => ( + + + + setFieldValue('geographicLocation', location) + } + disabled={isSubmitting} + renderInput={(params) => ( + + )} + /> + + + )} + + + ); +}; diff --git a/src/components/Shared/Forms/Accordions/AccordionEnum.ts b/src/components/Shared/Forms/Accordions/AccordionEnum.ts index cc006d1dde..f4c83b21b5 100644 --- a/src/components/Shared/Forms/Accordions/AccordionEnum.ts +++ b/src/components/Shared/Forms/Accordions/AccordionEnum.ts @@ -34,6 +34,7 @@ export enum PreferenceAccordion { DefaultAccount = 'DefaultAccount', EarlyAdopter = 'EarlyAdopter', ExportAllData = 'ExportAllData', + GeographicLocation = 'GeographicLocation', HomeCountry = 'HomeCountry', HourToSendNotifications = 'HourToSendNotifications', Language = 'Language', From 0bced3151849f8fd0f850d4ac1f18eefd0bf0ed6 Mon Sep 17 00:00:00 2001 From: Katelyn Grimes Date: Wed, 12 Aug 2026 14:29:18 -0400 Subject: [PATCH 2/9] Update geographic location when salary calculation submits --- .../settings/preferences.page.test.tsx | 13 +++- .../SalaryCalculatorTestWrapper.tsx | 11 +++ .../StepNavigation/StepNavigation.test.tsx | 74 +++++++++++++++++++ .../StepNavigation/StepNavigation.tsx | 36 +++++++++ .../SubmitModal/SubmitModal.test.tsx | 23 ++++++ .../SubmitModal/SubmitModal.tsx | 19 +++++ 6 files changed, 175 insertions(+), 1 deletion(-) diff --git a/pages/accountLists/[accountListId]/settings/preferences.page.test.tsx b/pages/accountLists/[accountListId]/settings/preferences.page.test.tsx index ad314571d4..1879ddc62d 100644 --- a/pages/accountLists/[accountListId]/settings/preferences.page.test.tsx +++ b/pages/accountLists/[accountListId]/settings/preferences.page.test.tsx @@ -286,9 +286,20 @@ describe('Preferences page', () => { ), ).toBeInTheDocument(); - // Home Country + // Geographic Location const skipButton = getByRole('button', { name: 'Skip Step' }); userEvent.click(skipButton); + expect( + await findByText('Are you within 50 miles of a major city?'), + ).toBeInTheDocument(); + expect( + await findByText( + 'This should be the major city within 50 miles of you. If none apply, leave this blank.', + ), + ).toBeInTheDocument(); + + // Home Country + userEvent.click(skipButton); expect( await findByText( 'This should be the place from which you are living and sending out physical communications. This will be used in exports for mailing address information.', diff --git a/src/components/HrTools/SalaryCalculator/SalaryCalculatorTestWrapper.tsx b/src/components/HrTools/SalaryCalculator/SalaryCalculatorTestWrapper.tsx index c8e7049c7d..4ce29e1fca 100644 --- a/src/components/HrTools/SalaryCalculator/SalaryCalculatorTestWrapper.tsx +++ b/src/components/HrTools/SalaryCalculator/SalaryCalculatorTestWrapper.tsx @@ -4,6 +4,7 @@ import { merge } from 'lodash'; import { DeepPartial } from 'ts-essentials'; import TestRouter from '__tests__/util/TestRouter'; import { GqlMockedProvider, gqlMock } from '__tests__/util/graphqlMocking'; +import { GetAccountPreferencesQuery } from 'src/components/Settings/preferences/GetAccountPreferences.generated'; import { StaffAccountQuery } from 'src/components/Shared/StaffAccount/StaffAccount.generated'; import { GetUserQuery } from 'src/components/User/GetUser.generated'; import { @@ -111,6 +112,7 @@ export interface SalaryCalculatorTestWrapperProps { editing?: boolean; userType?: UserTypeEnum; usStaffGroup?: UsStaffGroupEnum; + accountGeographicLocation?: string | null; } export const SalaryCalculatorTestWrapper: React.FC< @@ -127,6 +129,7 @@ export const SalaryCalculatorTestWrapper: React.FC< editing = true, userType = UserTypeEnum.UsStaff, usStaffGroup = UsStaffGroupEnum.SeniorStaff, + accountGeographicLocation = null, }) => { const hcmUserMerged = merge({}, hcmUserMock, hcmUser); const hcmSpouseMerged = merge({}, hcmSpouseMock, hcmSpouse); @@ -149,6 +152,7 @@ export const SalaryCalculatorTestWrapper: React.FC< GoalCalculatorConstants: GoalCalculatorConstantsQuery; StaffAccount: StaffAccountQuery; GetUser: GetUserQuery; + GetAccountPreferences: GetAccountPreferencesQuery; }> mocks={{ StaffAccount: { @@ -163,6 +167,13 @@ export const SalaryCalculatorTestWrapper: React.FC< usStaffGroup, }, }, + GetAccountPreferences: { + accountList: { + settings: { + geographicLocation: accountGeographicLocation, + }, + }, + }, PayrollDates: { payrollDates, }, diff --git a/src/components/HrTools/SalaryCalculator/StepNavigation/StepNavigation.test.tsx b/src/components/HrTools/SalaryCalculator/StepNavigation/StepNavigation.test.tsx index dac2b41edf..1182a65a76 100644 --- a/src/components/HrTools/SalaryCalculator/StepNavigation/StepNavigation.test.tsx +++ b/src/components/HrTools/SalaryCalculator/StepNavigation/StepNavigation.test.tsx @@ -21,6 +21,7 @@ jest.mock('src/components/Shared/Autosave/AutosaveForm', () => { }; }); +const location = 'Chicago, IL'; const mutationSpy = jest.fn(); const TestComponent: React.FC = (props) => ( @@ -152,4 +153,77 @@ describe('SubmitButton', () => { expect(mutationSpy).toHaveGraphqlOperation('SubmitSalaryCalculation'), ); }); + + it('updates the account geographic location preference when submitting', async () => { + const { findByText } = render( + + + , + ); + + userEvent.click(await findByText('Submit')); + userEvent.click(await findByText('Yes, Continue')); + + await waitFor(() => + expect(mutationSpy).toHaveGraphqlOperation('UpdateAccountPreferences', { + input: { + id: 'account-list-1', + attributes: { + id: 'account-list-1', + settings: { geographicLocation: location }, + }, + }, + }), + ); + }); + + it('does not update the account geographic location preference when the calculation has no location', async () => { + const { findByText, queryByRole } = render( + + + , + ); + + userEvent.click(await findByText('Submit')); + + await waitFor(() => + expect(queryByRole('checkbox')).not.toBeInTheDocument(), + ); + + userEvent.click(await findByText('Yes, Continue')); + + await waitFor(() => + expect(mutationSpy).toHaveGraphqlOperation('SubmitSalaryCalculation'), + ); + expect(mutationSpy).not.toHaveGraphqlOperation('UpdateAccountPreferences'); + }); + + it('does not show geographic location checkbox when it matches account preferences', async () => { + const { findByRole, queryByRole } = render( + + + , + ); + + userEvent.click(await findByRole('button', { name: 'Submit' })); + + await findByRole('dialog'); + await waitFor(() => + expect(queryByRole('checkbox')).not.toBeInTheDocument(), + ); + }); }); diff --git a/src/components/HrTools/SalaryCalculator/StepNavigation/StepNavigation.tsx b/src/components/HrTools/SalaryCalculator/StepNavigation/StepNavigation.tsx index c6adf6ef41..168757d82b 100644 --- a/src/components/HrTools/SalaryCalculator/StepNavigation/StepNavigation.tsx +++ b/src/components/HrTools/SalaryCalculator/StepNavigation/StepNavigation.tsx @@ -6,6 +6,8 @@ import { Box, Button, ButtonProps, Stack, Typography } from '@mui/material'; import { useTheme } from '@mui/material/styles'; import { useTranslation } from 'react-i18next'; import { SubmitModal } from 'src/components/HrTools/Shared/CalculationReports/SubmitModal/SubmitModal'; +import { useGetAccountPreferencesQuery } from 'src/components/Settings/preferences/GetAccountPreferences.generated'; +import { useUpdateAccountPreferencesMutation } from 'src/components/Settings/preferences/accordions/UpdateAccountPreferences.generated'; import { useAutosaveForm } from 'src/components/Shared/Autosave/AutosaveForm'; import { useAccountListId } from 'src/hooks/useAccountListId'; import { useSalaryCalculator } from '../SalaryCalculatorContext/SalaryCalculatorContext'; @@ -92,12 +94,24 @@ export const ContinueButton: React.FC = (props) => { export const SubmitButton: React.FC = (props) => { const { t } = useTranslation(); + const accountListId = useAccountListId(); const { handleNextStep, calculation } = useSalaryCalculator(); const [submit, { loading: submitting }] = useSubmitSalaryCalculationMutation(); const [submitDialogOpen, setSubmitDialogOpen] = useState(false); const { title, content, subContent } = useSubmitDialogContent(); + const { data } = useGetAccountPreferencesQuery({ + variables: { + accountListId, + }, + }); + const [updateAccountPreferences] = useUpdateAccountPreferencesMutation(); + + const updateGeographicLocation = + !!calculation?.location && + data?.accountList?.settings?.geographicLocation !== calculation?.location; + const handleSubmit = async () => { if (calculation) { await submit({ @@ -107,6 +121,23 @@ export const SubmitButton: React.FC = (props) => { }, }, }); + + if (updateGeographicLocation) { + await updateAccountPreferences({ + variables: { + input: { + id: accountListId, + attributes: { + id: accountListId, + settings: { + geographicLocation: calculation?.location, + }, + }, + }, + }, + }); + } + handleNextStep(); } }; @@ -135,6 +166,11 @@ export const SubmitButton: React.FC = (props) => { overrideContent={content} overrideSubContent={subContent} submitting={submitting} + geographicLocation={ + updateGeographicLocation + ? (calculation?.location ?? undefined) + : undefined + } /> )} diff --git a/src/components/HrTools/Shared/CalculationReports/SubmitModal/SubmitModal.test.tsx b/src/components/HrTools/Shared/CalculationReports/SubmitModal/SubmitModal.test.tsx index 4f736a4633..85d816b95f 100644 --- a/src/components/HrTools/Shared/CalculationReports/SubmitModal/SubmitModal.test.tsx +++ b/src/components/HrTools/Shared/CalculationReports/SubmitModal/SubmitModal.test.tsx @@ -23,6 +23,7 @@ const title = 'Test Title'; const content = 'Test Content'; const subContent = 'Test Sub Content'; const date = '2024-12-31'; +const location = 'Test Location'; const handleClose = jest.fn(); const handleConfirm = jest.fn(); @@ -85,6 +86,7 @@ interface TestComponentProps { additionalApproval?: boolean; splitAsr?: boolean; submitting?: boolean; + geographicLocation?: string; } const TestComponent: React.FC = ({ @@ -100,6 +102,7 @@ const TestComponent: React.FC = ({ additionalApproval, splitAsr, submitting, + geographicLocation, }) => ( @@ -122,6 +125,7 @@ const TestComponent: React.FC = ({ additionalApproval={additionalApproval} splitAsr={splitAsr} submitting={submitting} + geographicLocation={geographicLocation} /> @@ -288,6 +292,25 @@ describe('ConfirmationModal', () => { }); }); + describe('Geographic location checkbox', () => { + it('shows a checked, disabled checkbox naming the geographic location', async () => { + const { findByRole } = render( + , + ); + + const checkbox = await findByRole('checkbox', { name: /Test Location/i }); + expect(checkbox).toBeChecked(); + expect(checkbox).toBeDisabled(); + }); + + it('does not show checkbox when no geographic location is given', async () => { + const { findByRole, queryByRole } = render(); + + await findByRole('dialog'); + expect(queryByRole('checkbox')).not.toBeInTheDocument(); + }); + }); + describe('submitting state', () => { it('shows a loading spinner on the submit button while submitting', async () => { const { findByRole } = render( diff --git a/src/components/HrTools/Shared/CalculationReports/SubmitModal/SubmitModal.tsx b/src/components/HrTools/Shared/CalculationReports/SubmitModal/SubmitModal.tsx index d1ea607d4d..c834557762 100644 --- a/src/components/HrTools/Shared/CalculationReports/SubmitModal/SubmitModal.tsx +++ b/src/components/HrTools/Shared/CalculationReports/SubmitModal/SubmitModal.tsx @@ -4,11 +4,14 @@ import { Alert, Box, Button, + Checkbox, CircularProgress, Dialog, DialogActions, DialogContent, DialogTitle, + FormControlLabel, + FormGroup, } from '@mui/material'; import { DateTime } from 'luxon'; import { useTranslation } from 'react-i18next'; @@ -34,6 +37,7 @@ interface SubmitModalProps { splitAsr?: boolean; disableSubmit?: boolean; submitting?: boolean; + geographicLocation?: string; } export const SubmitModal: React.FC = ({ @@ -52,6 +56,7 @@ export const SubmitModal: React.FC = ({ splitAsr, disableSubmit, submitting, + geographicLocation, }) => { const { t } = useTranslation(); const locale = useLocale(); @@ -104,6 +109,20 @@ export const SubmitModal: React.FC = ({ )} + {geographicLocation && ( + + + } + label={t( + 'Your geographic location will be updated as {{geographicLocation}} in your account settings.', + { geographicLocation }, + )} + /> + + )}