Skip to content
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

[MPDX-8480] Health Indicator preferences page #1218

Merged
merged 9 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
query MachineCalculatedGoal($accountListId: ID!, $month: ISO8601Date!) {
healthIndicatorData(
accountListId: $accountListId
beginDate: $month
endDate: $month
) {
id
machineCalculatedGoal
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { SnackbarProvider } from 'notistack';
import TestRouter from '__tests__/util/TestRouter';
import { GqlMockedProvider } from '__tests__/util/graphqlMocking';
import theme from 'src/theme';
import { MachineCalculatedGoalQuery } from './MachineCalculatedGoal.generated';
import { MonthlyGoalAccordion } from './MonthlyGoalAccordion';

jest.mock('next-auth/react');
Expand Down Expand Up @@ -33,17 +34,30 @@ const mutationSpy = jest.fn();

interface ComponentsProps {
monthlyGoal: number | null;
machineCalculatedGoal?: number;
expandedPanel: string;
}

const Components: React.FC<ComponentsProps> = ({
monthlyGoal,
machineCalculatedGoal,
expandedPanel,
}) => (
<SnackbarProvider>
<TestRouter router={router}>
<ThemeProvider theme={theme}>
<GqlMockedProvider onCall={mutationSpy}>
<GqlMockedProvider<{
MachineCalculatedGoal: MachineCalculatedGoalQuery;
}>
mocks={{
MachineCalculatedGoal: {
healthIndicatorData: machineCalculatedGoal
? [{ machineCalculatedGoal }]
: [],
},
}}
onCall={mutationSpy}
>
<MonthlyGoalAccordion
handleAccordionChange={handleAccordionChange}
expandedPanel={expandedPanel}
Expand Down Expand Up @@ -134,4 +148,61 @@ describe('MonthlyGoalAccordion', () => {
]);
});
});

describe('calculated goal', () => {
it('resets goal to calculated goal', async () => {
const { getByRole, findByText } = render(
<Components
monthlyGoal={1000}
machineCalculatedGoal={1500}
expandedPanel={label}
/>,
);
const input = getByRole('spinbutton', { name: label });

expect(
await findByText(
'Based on the past year, NetSuite estimates that you need at least $1,500 of monthly support. You can use this amount or choose your own target monthly goal.',
),
).toBeInTheDocument();

const resetButton = getByRole('button', { name: /Reset/ });
userEvent.click(resetButton);
expect(input).toHaveValue(1500);
expect(resetButton).not.toBeInTheDocument();

await waitFor(() =>
expect(mutationSpy).toHaveGraphqlOperation('UpdateAccountPreferences', {
input: {
id: accountListId,
attributes: {
settings: {
monthlyGoal: 1500,
},
},
},
}),
);
});

it('hides reset button if goal matches calculated goal', async () => {
const { getByRole, findByText, queryByRole } = render(
<Components
monthlyGoal={1000}
machineCalculatedGoal={1000}
expandedPanel={label}
/>,
);

expect(
await findByText(
'Based on the past year, NetSuite estimates that you need at least $1,000 of monthly support. You can use this amount or choose your own target monthly goal.',
),
).toBeInTheDocument();
expect(queryByRole('button', { name: /Reset/ })).not.toBeInTheDocument();

userEvent.type(getByRole('spinbutton', { name: label }), '0');
expect(getByRole('button', { name: /Reset/ })).toBeInTheDocument();
});
});
});
Original file line number Diff line number Diff line change
@@ -1,23 +1,39 @@
import React, { ReactElement, useMemo } from 'react';
import { TextField } from '@mui/material';
import { Box, Button, TextField, Tooltip } from '@mui/material';
import { Formik } from 'formik';
import { DateTime } from 'luxon';
import { useSnackbar } from 'notistack';
import { useTranslation } from 'react-i18next';
import * as yup from 'yup';
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 { useLocale } from 'src/hooks/useLocale';
import { currencyFormat } from 'src/lib/intlFormat';
import { useUpdateAccountPreferencesMutation } from '../UpdateAccountPreferences.generated';
import { useMachineCalculatedGoalQuery } from './MachineCalculatedGoal.generated';

const accountPreferencesSchema: yup.ObjectSchema<
Pick<AccountListSettingsInput, 'monthlyGoal'>
> = yup.object({
monthlyGoal: yup.number().required(),
});

const formatMonthlyGoal = (
goal: number | null,
currency: string,
locale: string,
): string => {
if (goal === null) {
return '';
}

if (currency && locale) {
return currencyFormat(goal, currency, locale);
}
return goal.toString();
};

interface MonthlyGoalAccordionProps {
handleAccordionChange: (panel: string) => void;
expandedPanel: string;
Expand All @@ -43,13 +59,22 @@ export const MonthlyGoalAccordion: React.FC<MonthlyGoalAccordionProps> = ({
const locale = useLocale();
const label = t('Monthly Goal');

const monthlyGoalString = useMemo(() => {
return monthlyGoal && locale && currency
? currencyFormat(monthlyGoal, currency, locale)
: monthlyGoal
? String(monthlyGoal)
: '';
}, [monthlyGoal, locale, currency]);
const { data } = useMachineCalculatedGoalQuery({
variables: {
accountListId,
month: DateTime.now().startOf('month').toISODate(),
},
});
const calculatedGoal = data?.healthIndicatorData[0]?.machineCalculatedGoal;
const formattedCalculatedGoal = useMemo(
() => formatMonthlyGoal(calculatedGoal ?? null, currency, locale),
[calculatedGoal, currency, locale],
);

const formattedMonthlyGoal = useMemo(
() => formatMonthlyGoal(monthlyGoal, currency, locale),
[monthlyGoal, currency, locale],
);

const onSubmit = async (
attributes: Pick<AccountListSettingsInput, 'monthlyGoal'>,
Expand Down Expand Up @@ -79,12 +104,21 @@ export const MonthlyGoalAccordion: React.FC<MonthlyGoalAccordionProps> = ({
handleSetupChange();
};

const instructions = calculatedGoal
? t(
'Based on the past year, NetSuite estimates that you need at least {{goal}} of monthly support. You can use this amount or choose your own target monthly goal.',
{ goal: formattedCalculatedGoal },
)
: t(
'This amount should be set to the amount your organization has determined is your target monthly goal. If you do not know, make your best guess for now. You can change it at any time.',
);

return (
<AccordionItem
onAccordionChange={handleAccordionChange}
expandedPanel={expandedPanel}
label={label}
value={monthlyGoalString}
value={formattedMonthlyGoal}
fullWidth
disabled={disabled}
>
Expand All @@ -101,20 +135,14 @@ export const MonthlyGoalAccordion: React.FC<MonthlyGoalAccordionProps> = ({
values: { monthlyGoal },
errors,
handleSubmit,
submitForm,
isSubmitting,
isValid,
handleChange,
setFieldValue,
}): ReactElement => (
<FormWrapper
onSubmit={handleSubmit}
isValid={isValid}
isSubmitting={isSubmitting}
>
<FieldWrapper
helperText={t(
'This amount should be set to the amount your organization has determined is your target monthly goal. If you do not know, make your best guess for now. You can change it at any time.',
)}
>
<form onSubmit={handleSubmit}>
<FieldWrapper helperText={instructions}>
<TextField
value={monthlyGoal}
onChange={handleChange}
Expand All @@ -132,7 +160,38 @@ export const MonthlyGoalAccordion: React.FC<MonthlyGoalAccordionProps> = ({
id="monthlyGoalInput"
/>
</FieldWrapper>
</FormWrapper>
<Box display="flex" gap={2} mt={2}>
<Button
variant="contained"
color="primary"
type="submit"
disabled={!isValid || isSubmitting}
>
{t('Save')}
</Button>
{calculatedGoal && monthlyGoal !== calculatedGoal && (
<Tooltip
title={t(
'Reset to NetSuite estimated goal of {{calculatedGoal}}',
{
calculatedGoal: formattedCalculatedGoal,
},
)}
>
<Button
variant="outlined"
type="button"
onClick={() => {
setFieldValue('monthlyGoal', calculatedGoal);
submitForm();
}}
>
{t('Reset to Calculated Goal')}
</Button>
</Tooltip>
)}
</Box>
</form>
)}
</Formik>
</AccordionItem>
Expand Down
4 changes: 1 addition & 3 deletions src/components/Shared/Forms/FormWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ interface FormWrapperProps {
onSubmit: () => void;
isValid: boolean;
isSubmitting: boolean;
formAttrs?: { action?: string; method?: string };
children: React.ReactNode;
buttonText?: string;
}
Expand All @@ -16,15 +15,14 @@ export const FormWrapper: React.FC<FormWrapperProps> = ({
onSubmit,
isValid,
isSubmitting,
formAttrs = {},
children,
buttonText,
}) => {
const { t } = useTranslation();
const theme = useTheme();

return (
<form {...formAttrs} onSubmit={onSubmit}>
<form onSubmit={onSubmit}>
{children}
<Button
variant="contained"
Expand Down
Loading