Skip to content

Commit

Permalink
feat: add application alteration creation for handler UI (#3075) (HL-…
Browse files Browse the repository at this point in the history
…1253)
  • Loading branch information
EmiliaMakelaVincit authored Jun 11, 2024
1 parent 99d21c9 commit 1f1c84a
Show file tree
Hide file tree
Showing 39 changed files with 961 additions and 294 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { AxiosError } from 'axios';
import {
$AlterationFormButtonContainer,
$AlterationFormContainer,
} from 'benefit/applicant/components/applications/alteration/AlterationPage.sc';
import useAlterationForm from 'benefit/applicant/components/applications/alteration/useAlterationForm';
import { useTranslation } from 'benefit/applicant/i18n';
import AlterationForm from 'benefit-shared/components/alterationForm/AlterationForm';
import { $SaveActionFormErrorText } from 'benefit-shared/components/alterationForm/AlterationForm.sc';
import { ALTERATION_TYPE } from 'benefit-shared/constants';
import AlterationFormProvider from 'benefit-shared/context/AlterationFormProvider';
import {
Application,
ApplicationAlterationData,
} from 'benefit-shared/types/application';
import camelcaseKeys from 'camelcase-keys';
import { Button, IconAlertCircleFill, IconArrowRight } from 'hds-react';
import kebabCase from 'lodash/kebabCase';
import React from 'react';
import { useQueryClient } from 'react-query';
import { $Hr } from 'shared/components/forms/section/FormSection.sc';
import hdsToast from 'shared/components/toast/Toast';

type Props = {
onCancel: () => void;
onSuccess: () => void;
application: Application;
};

const AlterationFormContainer: React.FC<Props> = ({
onCancel,
onSuccess,
application,
}) => {
const queryClient = useQueryClient();
const { t } = useTranslation();

const handleSuccess = async (
response: ApplicationAlterationData
): Promise<void> => {
await queryClient.invalidateQueries(['applications', application.id]);
onSuccess();

const textKey =
response.alteration_type === ALTERATION_TYPE.TERMINATION
? 'common:notifications.alterationCreated.bodyTermination'
: 'common:notifications.alterationCreated.bodySuspension';

hdsToast({
autoDismissTime: 0,
type: 'success',
labelText: t('common:notifications.alterationCreated.title'),
text: t(textKey, {
id: application.applicationNumber,
}),
});
};

const handleError = (error: AxiosError<unknown>): void => {
const errorData = camelcaseKeys(error.response?.data ?? {});
const errors = [];

const getErrorItem = (
fieldKey: string,
itemKey: string,
value: string
): JSX.Element => {
const fieldLabel = t(
`common:applications.alterations.new.fields.${fieldKey}.label`,
''
);

if (!fieldLabel) {
return <li key={`${itemKey}`}>{value}</li>;
}

return (
<li key={`${itemKey}`}>
<a href={`#alteration-${kebabCase(fieldKey)}`}>
{fieldLabel}: {value}
</a>
</li>
);
};

Object.entries(errorData).forEach(([key, value]) => {
if (Array.isArray(value)) {
value.forEach((item: string) =>
errors.push(getErrorItem(key, `${key}_${item}`, item))
);
} else {
errors.push(getErrorItem(key, key, value as string));
}
});

hdsToast({
autoDismissTime: 0,
type: 'error',
labelText: t('common:error.generic.label'),
text: errors,
});
};

const alterationFormContextValues = useAlterationForm({
application,
onSuccess: handleSuccess,
onError: handleError,
});

const { handleSubmit, isSubmitting, isSubmitted, formik } =
alterationFormContextValues;

return (
<$AlterationFormContainer>
<AlterationFormProvider {...alterationFormContextValues}>
<AlterationForm application={application} />
</AlterationFormProvider>
<$Hr />
<$AlterationFormButtonContainer>
<Button theme="black" variant="secondary" onClick={onCancel}>
{t(`common:applications.alterations.new.actions.cancel`)}
</Button>
<Button
onClick={handleSubmit}
disabled={isSubmitting || (isSubmitted && !formik.isValid)}
theme="coat"
iconRight={<IconArrowRight />}
isLoading={isSubmitting}
loadingText={t(`common:utility.submitting`)}
>
{t(`common:applications.alterations.new.actions.submit`)}
</Button>
</$AlterationFormButtonContainer>
{isSubmitted && !formik.isValid && (
<$SaveActionFormErrorText>
<IconAlertCircleFill />
<p aria-live="polite">
{t('common:applications.errors.dirtyOrInvalidForm')}
</p>
</$SaveActionFormErrorText>
)}
</$AlterationFormContainer>
);
};

export default AlterationFormContainer;
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,11 @@ export const $PageHeading = styled.h1`
export const $AlterationFormContainer = styled.div`
margin-top: ${(props) => props.theme.spacingLayout.s};
`;

export const $AlterationFormButtonContainer = styled.div`
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: flex-start;
margin-top: ${(props) => props.theme.spacingLayout.s};
`;
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { AxiosError } from 'axios';
import AlterationFormContainer from 'benefit/applicant/components/applications/alteration/AlterationFormContainer';
import {
$AlterationFormContainer,
$BackButtonContainer,
$MainHeaderItem,
$PageHeader,
Expand All @@ -14,28 +13,22 @@ import {
$PageSubHeading,
$SpinnerContainer,
} from 'benefit/applicant/components/applications/Applications.sc';
import AlterationForm from 'benefit/applicant/components/applications/forms/application/alteration/AlterationForm';
import ErrorPage from 'benefit/applicant/components/errorPage/ErrorPage';
import { ROUTES } from 'benefit/applicant/constants';
import {
ALTERATION_STATE,
ALTERATION_TYPE,
APPLICATION_STATUSES,
} from 'benefit-shared/constants';
import { ApplicationAlterationData } from 'benefit-shared/types/application';
import { isTruthy } from 'benefit-shared/utils/common';
import camelcaseKeys from 'camelcase-keys';
import { Button, IconArrowLeft, LoadingSpinner } from 'hds-react';
import kebabCase from 'lodash/kebabCase';
import { useRouter } from 'next/router';
import React from 'react';
import { useQueryClient } from 'react-query';
import Container from 'shared/components/container/Container';
import {
$Grid,
$GridCell,
} from 'shared/components/forms/section/FormSection.sc';
import hdsToast from 'shared/components/toast/Toast';
import { convertToUIDateAndTimeFormat } from 'shared/utils/date.utils';
import { useTheme } from 'styled-components';

Expand All @@ -44,9 +37,11 @@ const AlterationPage = (): JSX.Element => {

const router = useRouter();
const theme = useTheme();
const queryClient = useQueryClient();

if (isLoading) {
const returnToApplication = (): void =>
void router.push(`${ROUTES.APPLICATION_FORM}?id=${id}`);

if (isLoading || (!isError && !application)) {
return (
<$SpinnerContainer>
<LoadingSpinner />
Expand All @@ -68,75 +63,6 @@ const AlterationPage = (): JSX.Element => {
);
}

const returnToApplication = (): void =>
void router.push(`${ROUTES.APPLICATION_FORM}?id=${id}`);

const onSuccess = async (
response: ApplicationAlterationData
): Promise<void> => {
await queryClient.invalidateQueries(['applications', application.id]);
returnToApplication();

const textKey =
response.alteration_type === ALTERATION_TYPE.TERMINATION
? 'common:notifications.alterationCreated.bodyTermination'
: 'common:notifications.alterationCreated.bodySuspension';

hdsToast({
autoDismissTime: 0,
type: 'success',
labelText: t('common:notifications.alterationCreated.title'),
text: t(textKey, {
id: application.applicationNumber,
}),
});
};

const onError = (error: AxiosError<unknown>): void => {
const errorData = camelcaseKeys(error.response?.data ?? {});
const errors = [];

const getErrorItem = (
fieldKey: string,
itemKey: string,
value: string
): JSX.Element => {
const fieldLabel = t(
`common:applications.alterations.new.fields.${fieldKey}.label`,
''
);

if (!fieldLabel) {
return <li key={`${itemKey}`}>{value}</li>;
}

return (
<li key={`${itemKey}`}>
<a href={`#alteration-${kebabCase(fieldKey)}`}>
{fieldLabel}: {value}
</a>
</li>
);
};

Object.entries(errorData).forEach(([key, value]) => {
if (Array.isArray(value)) {
value.forEach((item: string) =>
errors.push(getErrorItem(key, `${key}_${item}`, item))
);
} else {
errors.push(getErrorItem(key, key, value as string));
}
});

hdsToast({
autoDismissTime: 0,
type: 'error',
labelText: t('common:error.generic.label'),
text: errors,
});
};

const hasHandledTermination = application.alterations.some(
(alteration) =>
alteration.state === ALTERATION_STATE.HANDLED &&
Expand Down Expand Up @@ -194,14 +120,11 @@ const AlterationPage = (): JSX.Element => {
<p>{t('common:applications.pageHeaders.guideText')}</p>
</$GridCell>
</$Grid>
<$AlterationFormContainer>
<AlterationForm
application={application}
onCancel={returnToApplication}
onSuccess={onSuccess}
onError={onError}
/>
</$AlterationFormContainer>
<AlterationFormContainer
onCancel={returnToApplication}
onSuccess={returnToApplication}
application={application}
/>
</>
)}
{hasHandledTermination && (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AxiosError } from 'axios';
import useCreateApplicationAlterationQuery from 'benefit/applicant/hooks/useCreateApplicationAlterationQuery';
import { useTranslation } from 'benefit/applicant/i18n';
import { getValidationSchema } from 'benefit-shared/components/alterationForm/utils/validation';
import {
Application,
ApplicationAlteration,
Expand All @@ -15,8 +16,6 @@ import { Language } from 'shared/i18n/i18n';
import { convertDateFormat } from 'shared/utils/date.utils';
import snakecaseKeys from 'snakecase-keys';

import { getValidationSchema } from '../../../alteration/utils/validation';

type Props = {
application: Application;
onSuccess?: MutationFunction<void, ApplicationAlterationData>;
Expand Down Expand Up @@ -51,11 +50,14 @@ const useAlterationForm = ({
});

const submitForm = (data: ApplicationAlteration): void => {
const payload = snakecaseKeys({
...data,
endDate: convertDateFormat(data.endDate),
resumeDate: convertDateFormat(data.resumeDate) || undefined,
}, { deep: true }) as ApplicationAlterationData;
const payload = snakecaseKeys(
{
...data,
endDate: convertDateFormat(data.endDate),
resumeDate: convertDateFormat(data.resumeDate) || undefined,
},
{ deep: true }
) as ApplicationAlterationData;

createQuery(payload);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import { TFunction } from 'next-i18next';
import { useEffect, useState } from 'react';

type AlterationPageProps = {
application: Application | null,
t: TFunction,
id: string,
isLoading: boolean,
isError: boolean,
application: Application | null;
t: TFunction;
id: string;
isLoading: boolean;
isError: boolean;
};

const isApplicationLoaded = (id: number | string, status: string): boolean =>
Expand All @@ -23,7 +23,6 @@ const useAlterationPage = (): AlterationPageProps => {
const { t } = useTranslation();

const [isLoading, setIsLoading] = useState(true);
// query param used in edit mode. id from context used for updating newly created application
const {
status: existingApplicationStatus,
data: existingApplication,
Expand All @@ -48,7 +47,7 @@ const useAlterationPage = (): AlterationPageProps => {
application: camelcaseKeys(existingApplication, { deep: true }) ?? null,
isLoading,
isError: !!existingApplicationError,
}
}
};
};

export default useAlterationPage;
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import DeMinimisContext from 'benefit/applicant/context/DeMinimisContext';
import { useTranslation } from 'benefit/applicant/i18n';
import { getErrorText } from 'benefit/applicant/utils/forms';
import { DE_MINIMIS_AID_KEYS } from 'benefit-shared/constants';
import { DeMinimisAid } from 'benefit-shared/types/application';
import { getErrorText } from 'benefit-shared/utils/forms';
import { FormikProps, useFormik } from 'formik';
import fromPairs from 'lodash/fromPairs';
import { TFunction } from 'next-i18next';
Expand Down
Loading

0 comments on commit 1f1c84a

Please sign in to comment.