Skip to content

Commit

Permalink
feat: edit application form with handler GUI (HL-990) (#2764)
Browse files Browse the repository at this point in the history
* chore: route change /new-application to /application/new

* refactor: hoist another FC out of parent component's scope

* feat: new page for application edit

* fix: increase margin of application header as in design

* feat: add edit buttons for each review section

* fix: allow update action even if calculation is not complete

* feat: improve error handling

* feat: show application header if it is edited

* test: submit handler-created form

* test: write browser test to edit application

* feat: add and update translations

* fix: lint batch file that was missing previously
  • Loading branch information
sirtawast authored Jan 30, 2024
1 parent f481829 commit 40cb66f
Show file tree
Hide file tree
Showing 72 changed files with 1,002 additions and 504 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1146,10 +1146,9 @@ def _update_applicant_terms_approval(self, instance, approve_terms):
action = data["action"] if "action" in data else None

# Ignore applicant's terms if app origin is from handler
if (
instance.application_origin == ApplicationOrigin.HANDLER
or action == ApplicationActions.APPLICANT_TOGGLE_EDIT
):
if instance.application_origin == ApplicationOrigin.HANDLER or action in [
ApplicationActions.HANDLER_ALLOW_APPLICATION_EDIT
]:
return

if not approve_terms:
Expand Down
4 changes: 2 additions & 2 deletions backend/benefit/applications/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,8 @@ class AhjoStatus(models.TextChoices):


class ApplicationActions(models.TextChoices):
APPLICANT_TOGGLE_EDIT = "APPLICANT_TOGGLE_EDIT", _(
"Allow/disallow applicant's modifications"
HANDLER_ALLOW_APPLICATION_EDIT = "HANDLER_ALLOW_APPLICATION_EDIT", _(
"Allow/disallow applicant's modifications to the application"
)


Expand Down
2 changes: 1 addition & 1 deletion backend/benefit/calculator/api/v1/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ def validate(self, data):
if "action" in request.data:
action = request.data["action"]
if self._are_dates_required() and action not in [
ApplicationActions.APPLICANT_TOGGLE_EDIT
ApplicationActions.HANDLER_ALLOW_APPLICATION_EDIT,
]:
if data.get("start_date") is None:
raise serializers.ValidationError(
Expand Down
9 changes: 2 additions & 7 deletions frontend/benefit/applicant/src/hooks/useFormActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
ApplicationData,
Employee,
} from 'benefit-shared/types/application';
import { prettyPrintObject } from 'benefit-shared/utils/errors';
import camelcaseKeys from 'camelcase-keys';
import { useRouter } from 'next/router';
import React, { useContext, useEffect } from 'react';
Expand All @@ -34,12 +35,6 @@ interface FormActions {
onDelete: (id: string) => void;
}

const prettyPrintObject = (object: Record<string, string[]>): string =>
JSON.stringify(object)
.replace(/["[\]{}]/g, '')
.replace(/:/g, ': ')
.replace(/,/g, '\n');

const useFormActions = (application: Partial<Application>): FormActions => {
const router = useRouter();
const currentStep = getApplicationStepFromString(
Expand Down Expand Up @@ -91,7 +86,7 @@ const useFormActions = (application: Partial<Application>): FormActions => {
{value}
</a>
) : (
prettyPrintObject(value)
prettyPrintObject({ data: value })
)
),
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { clearDataToPrintOnFailure } from '@frontend/shared/browser-tests/utils/testcafe.utils';
import { DATE_FORMATS } from '@frontend/shared/src/utils/date.utils';
import { format } from 'date-fns';
import { Selector } from 'testcafe';

import fi from '../../public/locales/fi/common.json';
import MainIngress from '../page-model/MainIngress';
import handlerUser from '../utils/handlerUser';
import { getFrontendUrl } from '../utils/url.utils';

const form = {
company: {
id: '0201256-6',
bankAccountNumber: 'FI81 4975 4587 0004 02',
firstName: 'Kuura',
lastName: 'Massi-Päällikkö',
phone: '050 000 0000',
email: '[email protected]',
coOperationNegotiationsDescription: 'Lorem ipsum dolor sit amet',
},
employee: {
firstName: 'Ruu',
lastName: 'Rättisitikka',
ssn: '050632-8912',
monthlyPay: '1800',
vacationMoney: '100',
otherExpenses: '300',
jobTitle: 'Verkkosamooja',
workingHours: '30',
collectiveBargainingAgreement: 'Yleinen TES',
},
deminimis: {
granter: 'Valtio',
amount: '2000',
grantedAt: `1.1.${new Date().getFullYear()}`,
},
};

const url = getFrontendUrl(`/`);

const uploadFileAttachment = async (
t: TestController,
selector: string,
filename = 'sample.pdf'
) => {
await t.scrollIntoView(Selector(selector).parent(), { offsetY: -200 });
await t.setFilesToUpload(selector, filename);
await t.wait(100);
};

fixture('New application')
.page(url)
.beforeEach(async (t) => {
clearDataToPrintOnFailure(t);
await t.useRole(handlerUser);
await t.navigateTo('/');
});

test('Fill form and submit', async (t: TestController) => {
const mainIngress = new MainIngress(fi.mainIngress.heading, 'h1');
await mainIngress.isLoaded();

// Start filling form manually
await t.click('[data-testid="new-application-button"]');

// Organization info
const searchCompanyInput = Selector(
'[data-testid="company-search-input"]'
).find('input');
await t.typeText(searchCompanyInput, form.company.id).pressKey('enter');

// Company info
await t.typeText('#companyBankAccountNumber', form.company.bankAccountNumber);
await t.typeText('#companyContactPersonFirstName', form.company.firstName);
await t.typeText('#companyContactPersonLastName', form.company.lastName);
await t.typeText('#companyContactPersonPhoneNumber', form.company.phone);
await t.typeText('#companyContactPersonEmail', form.company.email);

// De minimis aid
await t.click('[for="deMinimisAidTrue"]');
await t.typeText('#granter', form.deminimis.granter);
await t.typeText('#amount', form.deminimis.amount);
await t.typeText('#grantedAt', form.deminimis.grantedAt);
const addButton = Selector('main button span')
.withText(fi.applications.sections.deMinimisAidsAdd)
.parent();
await t.click(addButton);

await t.click('[for="coOperationNegotiationsTrue"]');
await t.typeText(
'#coOperationNegotiationsDescription',
form.company.coOperationNegotiationsDescription
);

// Employee info
await t.typeText('[name="employee.firstName"]', form.employee.firstName);
await t.typeText('[name="employee.lastName"]', form.employee.lastName);
await t.typeText('[name="employee.socialSecurityNumber"]', form.employee.ssn);
await t.click('[for="employee.isLivingInHelsinki"]');

await t.typeText('[name="employee.jobTitle"]', form.employee.jobTitle);
await t.typeText(
'[name="employee.workingHours"]',
form.employee.workingHours
);
await t.typeText(
'[name="employee.collectiveBargainingAgreement"]',
form.employee.collectiveBargainingAgreement
);

await t.typeText('[name="employee.monthlyPay"]', form.employee.monthlyPay);
await t.typeText(
'[name="employee.vacationMoney"]',
form.employee.vacationMoney
);
await t.typeText(
'[name="employee.otherExpenses"]',
form.employee.otherExpenses
);

await t.click('[for="paySubsidyGranted.granted"]');
await t.click('[for="apprenticeshipProgramTrue"]');

await t.typeText(
'[name="startDate"]',
format(new Date(), DATE_FORMATS.UI_DATE)
);

await uploadFileAttachment(t, '#upload_attachment_full_application');
await uploadFileAttachment(t, '#upload_attachment_employment_contract');
await uploadFileAttachment(t, '#upload_attachment_pay_subsidy_decision');
await uploadFileAttachment(t, '#upload_attachment_education_contract');

/**
* Click through all applicant terms.
* Assume terms are loaded from fixture default_terms.json using LOAD_DEFAULT_TERMS=1
*/
await t.click(Selector('[name="application_consent_0"]'));
await t.click(Selector('[name="application_consent_1"]'));
await t.click(Selector('[name="application_consent_2"]'));
await t.click(Selector('[name="application_consent_3"]'));

// Validate form and submit
const buttonSelector = 'main button';
const nextButton = Selector(buttonSelector).withText(
fi.applications.actions.continue
);
await t.click(nextButton);

const submitButton = Selector(buttonSelector).withText(
fi.applications.actions.send
);
await t.expect(submitButton.visible).ok();
await t.click(submitButton);

// Form submitted, single application view shown
const handleButton = Selector(buttonSelector).withText(
fi.review.actions.handle
);
await t.expect(handleButton.visible).ok();
});
Loading

0 comments on commit 40cb66f

Please sign in to comment.