-
Notifications
You must be signed in to change notification settings - Fork 1
[MPDX-9475] - Add Default and Simple view to PDS Goal Calculator #1765
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Changes from all commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
23f2138
Add formType to PdsGoalCalculationFields fragment
wjames111 30b419c
Zero out reimbursable and 403b in summary data when formType is Simple
wjames111 79ade1f
Exclude reimbursable and 403b from PDS goal total when formType is Si…
wjames111 b7f4c38
Filter Reimbursable Expenses step from useSteps when formType is Simple
wjames111 df39d3a
Clamp PDS stepIndex when steps array shrinks (defensive)
wjames111 9461d65
Add Form Type select and conditionally hide 403b field in SetupStep
wjames111 cc2d616
Constrain formType Yup schema to enum values; cover null formType leg…
wjames111 6f96c65
Hide Reimbursable Expenses and 403b summary rows when formType is Simple
wjames111 baeb297
Show Default/Simple badge on PdsGoalCard
wjames111 0e08043
Add CreateGoalDialog for picking form type at goal creation
wjames111 33e001a
Add visually-hidden FormLabel to CreateGoalDialog fieldset for a11y
wjames111 023fa02
Open CreateGoalDialog from PdsGoalsList; pass formType to create muta…
wjames111 29c0810
Add error handling, state reset, and aria-labelledby to PDS goal crea…
wjames111 da99bac
PR fixes
wjames111 e3d03e5
More PR fixes
wjames111 ea5386d
Fix chip styling on goalCard
wjames111 2ebf482
Hide reimbursable expenses and 403b references
wjames111 ee14bb1
Fix premature validation
wjames111 39d8493
Replace useState with formik
wjames111 ca41ce9
Remove backward compat on PDS goal types
wjames111 1f48081
More PR fixes
wjames111 da951cb
Runs prettier
wjames111 0510eec
And more PR fixes...
wjames111 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
108 changes: 108 additions & 0 deletions
108
src/components/HrTools/PdsGoalCalculator/GoalsList/CreateGoalDialog.test.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| import React from 'react'; | ||
| import { ThemeProvider } from '@mui/material/styles'; | ||
| import { render, waitFor } from '@testing-library/react'; | ||
| import userEvent from '@testing-library/user-event'; | ||
| import { DesignationSupportFormType } from 'src/graphql/types.generated'; | ||
| import theme from 'src/theme'; | ||
| import { CreateGoalDialog } from './CreateGoalDialog'; | ||
|
|
||
| const renderDialog = ( | ||
| props: Partial<React.ComponentProps<typeof CreateGoalDialog>> = {}, | ||
| ) => { | ||
| const defaults = { | ||
| open: true, | ||
| onClose: jest.fn(), | ||
| onCreate: jest.fn().mockResolvedValue(undefined), | ||
| }; | ||
| const utils = render( | ||
| <ThemeProvider theme={theme}> | ||
| <CreateGoalDialog {...defaults} {...props} /> | ||
| </ThemeProvider>, | ||
| ); | ||
| const rerenderDialog = ( | ||
| nextProps: Partial<React.ComponentProps<typeof CreateGoalDialog>> = {}, | ||
| ) => | ||
| utils.rerender( | ||
| <ThemeProvider theme={theme}> | ||
| <CreateGoalDialog {...defaults} {...nextProps} /> | ||
| </ThemeProvider>, | ||
| ); | ||
| return { ...utils, rerenderDialog }; | ||
| }; | ||
|
|
||
| describe('CreateGoalDialog', () => { | ||
| it('renders both form-type options with descriptions', () => { | ||
| const { getByRole, getByText } = renderDialog(); | ||
| expect(getByRole('radio', { name: /Default/ })).toBeInTheDocument(); | ||
| expect(getByRole('radio', { name: /Simple/ })).toBeInTheDocument(); | ||
| expect( | ||
| getByText( | ||
| 'Full calculator with reimbursable expenses and 403b contributions.', | ||
| ), | ||
| ).toBeInTheDocument(); | ||
| expect( | ||
| getByText( | ||
| 'Streamlined calculator without reimbursable expenses or 403b contributions.', | ||
| ), | ||
| ).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('disables the Create button until a form type is picked', () => { | ||
| const { getByRole } = renderDialog(); | ||
| expect(getByRole('button', { name: 'Create' })).toBeDisabled(); | ||
| }); | ||
|
|
||
| it('enables Create after a form type is picked', () => { | ||
| const { getByRole } = renderDialog(); | ||
| userEvent.click(getByRole('radio', { name: /Simple/ })); | ||
| expect(getByRole('button', { name: 'Create' })).toBeEnabled(); | ||
| }); | ||
|
|
||
| it('calls onCreate with the chosen form type when Create is clicked', async () => { | ||
| const onCreate = jest.fn().mockResolvedValue(undefined); | ||
| const { getByRole } = renderDialog({ onCreate }); | ||
|
|
||
| userEvent.click(getByRole('radio', { name: /Simple/ })); | ||
| userEvent.click(getByRole('button', { name: 'Create' })); | ||
|
|
||
| await waitFor(() => | ||
| expect(onCreate).toHaveBeenCalledWith(DesignationSupportFormType.Simple), | ||
| ); | ||
| }); | ||
|
|
||
| it('calls onClose when Cancel is clicked', () => { | ||
| const onClose = jest.fn(); | ||
| const { getByRole } = renderDialog({ onClose }); | ||
| userEvent.click(getByRole('button', { name: 'Cancel' })); | ||
| expect(onClose).toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('disables Create but keeps Cancel enabled while submitting', async () => { | ||
| const onCreate = jest.fn().mockReturnValue(new Promise<void>(() => {})); | ||
| const { getByRole, findByRole } = renderDialog({ onCreate }); | ||
|
|
||
| userEvent.click(getByRole('radio', { name: /Simple/ })); | ||
| userEvent.click(getByRole('button', { name: 'Create' })); | ||
|
|
||
| await findByRole('progressbar'); | ||
| expect(getByRole('button', { name: 'Create' })).toBeDisabled(); | ||
| expect(getByRole('button', { name: 'Cancel' })).toBeEnabled(); | ||
| }); | ||
|
|
||
| it('does not render when open is false', () => { | ||
| const { queryByRole } = renderDialog({ open: false }); | ||
| expect(queryByRole('dialog')).not.toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('clears the selected option when reopened after Cancel', () => { | ||
| const { getByRole, rerenderDialog } = renderDialog(); | ||
|
|
||
| userEvent.click(getByRole('radio', { name: /Simple/ })); | ||
| userEvent.click(getByRole('button', { name: 'Cancel' })); | ||
|
|
||
| rerenderDialog({ open: false }); | ||
| rerenderDialog({ open: true }); | ||
|
|
||
| expect(getByRole('button', { name: 'Create' })).toBeDisabled(); | ||
| }); | ||
| }); |
171 changes: 171 additions & 0 deletions
171
src/components/HrTools/PdsGoalCalculator/GoalsList/CreateGoalDialog.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,171 @@ | ||
| import React, { useEffect, useRef } from 'react'; | ||
| import { | ||
| Button, | ||
| CircularProgress, | ||
| Dialog, | ||
| DialogActions, | ||
| DialogContent, | ||
| DialogTitle, | ||
| FormControl, | ||
| FormControlLabel, | ||
| FormLabel, | ||
| Radio, | ||
| RadioGroup, | ||
| Typography, | ||
| } from '@mui/material'; | ||
| import { visuallyHidden } from '@mui/utils'; | ||
| import { Formik, FormikProps } from 'formik'; | ||
| import { useTranslation } from 'react-i18next'; | ||
| import * as yup from 'yup'; | ||
| import { DesignationSupportFormType } from 'src/graphql/types.generated'; | ||
|
|
||
| export interface CreateGoalDialogProps { | ||
| open: boolean; | ||
| onClose: () => void; | ||
| onCreate: (formType: DesignationSupportFormType) => Promise<void>; | ||
| } | ||
|
|
||
| interface FormValues { | ||
| formType: DesignationSupportFormType | ''; | ||
| } | ||
|
|
||
| const schema = yup.object({ | ||
| formType: yup | ||
| .string() | ||
| .oneOf(Object.values(DesignationSupportFormType)) | ||
| .required(), | ||
| }); | ||
|
|
||
| export const CreateGoalDialog: React.FC<CreateGoalDialogProps> = ({ | ||
| open, | ||
| onClose, | ||
| onCreate, | ||
| }) => { | ||
| const { t } = useTranslation(); | ||
| const formikRef = useRef<FormikProps<FormValues>>(null); | ||
|
|
||
| useEffect(() => { | ||
| if (open) { | ||
| formikRef.current?.resetForm(); | ||
| } | ||
| }, [open]); | ||
|
|
||
| const formTypeOptions: Array<{ | ||
| value: DesignationSupportFormType; | ||
| title: string; | ||
| description: string; | ||
| }> = [ | ||
| { | ||
| value: DesignationSupportFormType.Detailed, | ||
| title: t('Default'), | ||
| description: t( | ||
| 'Full calculator with reimbursable expenses and 403b contributions.', | ||
| ), | ||
| }, | ||
| { | ||
| value: DesignationSupportFormType.Simple, | ||
| title: t('Simple'), | ||
| description: t( | ||
| 'Streamlined calculator without reimbursable expenses or 403b contributions.', | ||
| ), | ||
| }, | ||
| ]; | ||
|
|
||
| return ( | ||
| <Dialog | ||
| open={open} | ||
| onClose={onClose} | ||
| maxWidth="sm" | ||
| fullWidth | ||
| aria-labelledby="create-goal-dialog-title" | ||
| > | ||
| <DialogTitle id="create-goal-dialog-title"> | ||
| {t('Create a New Goal')} | ||
| </DialogTitle> | ||
| <Formik<FormValues> | ||
| innerRef={formikRef} | ||
| initialValues={{ formType: '' }} | ||
| validationSchema={schema} | ||
| onSubmit={async ({ formType }) => { | ||
| if (formType) { | ||
| await onCreate(formType); | ||
| } | ||
| }} | ||
| > | ||
| {({ values, isSubmitting, handleChange, handleSubmit }) => ( | ||
| <form onSubmit={handleSubmit}> | ||
| <DialogContent> | ||
| <FormControl component="fieldset"> | ||
| <FormLabel sx={visuallyHidden}> | ||
| {t('Select a form type')} | ||
| </FormLabel> | ||
| <RadioGroup | ||
| name="formType" | ||
| value={values.formType} | ||
| onChange={handleChange} | ||
| > | ||
| {formTypeOptions.map( | ||
| ({ value, title, description }, index) => ( | ||
| <FormControlLabel | ||
| key={value} | ||
| value={value} | ||
| control={ | ||
| <Radio | ||
| inputProps={{ | ||
| 'aria-labelledby': `${value}-title`, | ||
| 'aria-describedby': `${value}-desc`, | ||
| }} | ||
| /> | ||
| } | ||
| label={ | ||
| <> | ||
| <Typography | ||
| id={`${value}-title`} | ||
| variant="subtitle1" | ||
| component="span" | ||
| display="block" | ||
| > | ||
| {title} | ||
| </Typography> | ||
| <Typography | ||
| id={`${value}-desc`} | ||
| variant="body2" | ||
| color="text.secondary" | ||
| component="span" | ||
| display="block" | ||
| > | ||
| {description} | ||
| </Typography> | ||
| </> | ||
| } | ||
| sx={{ | ||
| alignItems: 'flex-start', | ||
| mb: index < formTypeOptions.length - 1 ? 2 : 0, | ||
| }} | ||
| /> | ||
| ), | ||
| )} | ||
| </RadioGroup> | ||
| </FormControl> | ||
| </DialogContent> | ||
| <DialogActions> | ||
| <Button onClick={onClose}>{t('Cancel')}</Button> | ||
| <Button | ||
| type="submit" | ||
| variant="contained" | ||
| disabled={!values.formType || isSubmitting} | ||
| startIcon={ | ||
| isSubmitting ? ( | ||
| <CircularProgress size={16} color="inherit" /> | ||
| ) : null | ||
| } | ||
| > | ||
| {t('Create')} | ||
| </Button> | ||
| </DialogActions> | ||
| </form> | ||
| )} | ||
| </Formik> | ||
| </Dialog> | ||
| ); | ||
| }; | ||
|
Check warning on line 171 in src/components/HrTools/PdsGoalCalculator/GoalsList/CreateGoalDialog.tsx
|
||
1 change: 1 addition & 0 deletions
1
src/components/HrTools/PdsGoalCalculator/GoalsList/PdsGoalCalculations.graphql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
calculatePdsGoalTotal()returnsotherExpenses.assessmentonly (the assessment line:(subtotal + creditCardFees + attrition) * adminRate).usePdsSummaryData.overallTotal = otherTotals.subtotal + otherTotals.attrition + otherTotals.creditCardFees + otherTotals.assessment.expect(result).toBeCloseTo(1038.21, 1)forcalculatePdsGoalTotalvsexpect(overallTotal).toBeCloseTo(9815.77, 0).Pre-dates this PR (introduced in commit
edfa70868per the goal-card landing PR #1755), but this PR makes the inconsistency more visible by adding the Default/Simple badge directly next to the card amount. Staff comparing card-vs-summary will see two very different numbers labeled "goal."Recommended follow-up (not blocking this PR):
PdsGoalCard.goalTotalto useusePdsSummaryData(goal, hcmUser).overallTotal, or renamecalculatePdsGoalTotaltocalculatePdsAssessmentand add acalculatePdsGoalOverallTotalhelper. Current naming is semantically misleading.Flag for a separate ticket — does not block this PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bump