diff --git a/backend/benefit/applications/api/v1/serializers/application.py b/backend/benefit/applications/api/v1/serializers/application.py index e833c12314..351857c72c 100755 --- a/backend/benefit/applications/api/v1/serializers/application.py +++ b/backend/benefit/applications/api/v1/serializers/application.py @@ -58,10 +58,7 @@ ArchivalApplication, Employee, ) -from applications.services.change_history import ( - get_application_change_history_made_by_applicant, - get_application_change_history_made_by_handler, -) +from applications.services.change_history import get_application_change_history from calculator.api.v1.serializers import ( CalculationSearchSerializer, CalculationSerializer, @@ -1651,10 +1648,7 @@ def get_ahjo_error(self, obj): return self.get_latest_ahjo_error(obj) def get_changes(self, obj): - return { - "handler": get_application_change_history_made_by_handler(obj), - "applicant": get_application_change_history_made_by_applicant(obj), - } + return get_application_change_history(obj) def get_company_for_new_application(self, _): """ diff --git a/backend/benefit/applications/services/change_history.py b/backend/benefit/applications/services/change_history.py index fdce337424..e06dfa99d3 100644 --- a/backend/benefit/applications/services/change_history.py +++ b/backend/benefit/applications/services/change_history.py @@ -1,24 +1,15 @@ -from datetime import datetime, timedelta +from datetime import timedelta from django.db.models import Q -from django.utils.translation import gettext_lazy as _ from simple_history.models import ModelChange from applications.enums import ApplicationStatus -from applications.models import ( - Application, - ApplicationLogEntry, - Attachment, - DeMinimisAid, - Employee, -) -from shared.audit_log.models import AuditLogEntry +from applications.models import Application, ApplicationLogEntry from users.models import User DISABLE_DE_MINIMIS_AIDS = True EXCLUDED_APPLICATION_FIELDS = ( "application_step", - "status", "pay_subsidy_percent", ) @@ -29,13 +20,6 @@ ) -def _get_handlers(as_ids: bool = False) -> list: - return [ - (user.id if as_ids else user) - for user in User.objects.filter(Q(is_staff=True) | Q(is_superuser=True)) - ] - - def _parse_change_values(value, field): if value is None: return None @@ -60,139 +44,6 @@ def _get_field_name(relation_name): } -def _get_application_change_history_between_timestamps( - ts_start: datetime, ts_end: datetime, application: Application -) -> list: - """ - Change history between two timestamps. Related objects handled here - separately (employee, attachments, de_minimis_aid_set). - - One-to-many related objects are handled in a way if there's new or modified - objects between the above mentioned status changes, the field in question - is considered changed. In this case field is added to the changes list and - old/new values set to None. The fields that are like this are attachments - and de_minimis_aid_set. - """ - employee = application.employee - try: - hist_application_when_start_editing = application.history.as_of( - ts_start - )._history - hist_application_when_stop_editing = application.history.as_of(ts_end)._history - hist_employee_when_start_editing = employee.history.as_of(ts_start)._history - hist_employee_when_stop_editing = employee.history.as_of(ts_end)._history - except Employee.DoesNotExist: - return [] - except Application.DoesNotExist: - return [] - - new_or_edited_attachments = Attachment.objects.filter( - Q(created_at__gte=ts_start) | Q(modified_at=ts_start), - Q(created_at__lte=ts_end) | Q(modified_at__lte=ts_end), - application=application, - ) - - new_or_edited_de_minimis_aids = [] - if not DISABLE_DE_MINIMIS_AIDS: - new_or_edited_de_minimis_aids = DeMinimisAid.objects.filter( - Q(created_at__gte=ts_start) | Q(modified_at=ts_start), - Q(created_at__lte=ts_end) | Q(modified_at__lte=ts_end), - application=application, - ) - - application_delta = hist_application_when_stop_editing.diff_against( - hist_application_when_start_editing, - excluded_fields=EXCLUDED_APPLICATION_FIELDS, - ) - - employee_delta = hist_employee_when_stop_editing.diff_against( - hist_employee_when_start_editing, - excluded_fields=EXCLUDED_EMPLOYEE_FIELDS, - ) - - changes = [] - - changes += [ - _format_change_dict( - change, - ) - for change in application_delta.changes - ] - - changes += [ - _format_change_dict(change, relation_name="employee") - for change in employee_delta.changes - ] - if new_or_edited_attachments: - for attachment in new_or_edited_attachments: - changes.append( - { - "field": "attachments", - "old": None, - "new": f"{attachment.attachment_file.name} ({_(attachment.attachment_type)})", - } - ) - - if not DISABLE_DE_MINIMIS_AIDS and new_or_edited_de_minimis_aids: - changes.append({"field": "de_minimis_aid_set", "old": None, "new": None}) - - if not changes: - return [] - - new_record = application_delta.new_record - return [ - { - "date": new_record.history_date, - "user": ( - f"{new_record.history_user.first_name} {new_record.history_user.last_name}" - if new_record.history_user - and new_record.history_user.first_name - and new_record.history_user.last_name - else "Unknown user" - ), - "reason": "", - "changes": changes, - } - ] - - -def get_application_change_history_made_by_applicant(application: Application) -> list: - """ - Get change history for application comparing historic application objects between - the last time status was changed from handling to additional_information_needed - and back to handling. This procudes a list of changes that are made by applicant - when status is additional_information_needed. - - NOTE: As the de minimis aids are always removed and created again when - updated (BaseApplicationSerializer -> _update_de_minimis_aid()), this - solution always thinks that de minimis aids are changed. - That's why tracking de minimis aids are disabled for now. - """ - application_log_entries = ApplicationLogEntry.objects.filter( - application=application - ) - - log_entry_start = ( - application_log_entries.filter(from_status="handling") - .filter(to_status="additional_information_needed") - .last() - ) - log_entry_end = ( - application_log_entries.filter(from_status="additional_information_needed") - .filter(to_status="handling") - .last() - ) - - if not log_entry_start or not log_entry_end: - return [] - - ts_start = log_entry_start.created_at - ts_end = log_entry_end.created_at - return _get_application_change_history_between_timestamps( - ts_start, ts_end, application - ) - - def _is_history_change_excluded(change, excluded_fields): return change.field in excluded_fields or change.old == change.new @@ -201,13 +52,16 @@ def _get_change_set_base(new_record: ModelChange): return { "date": new_record.history_date, "reason": new_record.history_change_reason, - "user": ( - f"{new_record.history_user.first_name} {new_record.history_user.last_name[0]}." - if new_record.history_user - and new_record.history_user.first_name - and new_record.history_user.last_name - else "Unknown user" - ), + "user": { + "staff": getattr(new_record.history_user, "is_staff", False), + "name": ( + f"{new_record.history_user.first_name} {new_record.history_user.last_name[0]}." + if new_record.history_user + and new_record.history_user.first_name + and new_record.history_user.last_name + else "Unknown user" + ), + }, "changes": [], } @@ -257,25 +111,27 @@ def _create_change_set(app_diff, employee_diffs): ) -def get_application_change_history_made_by_handler(application: Application) -> list: +def get_application_change_history(application: Application) -> list: """ Get application change history between the point when application is received and - the current time. If the application has been in status - additional_information_needed, changes made then are not included. - This solution should work for getting changes made by handler. + the current time. - NOTE: The same de minimis aid restriction here, so they are not tracked. - Also, changes made when application status is additional_information_needed are - not tracked, even if they are made by handler. + NOTE: The de minimis aid is not tracked here. """ - # Get all edits made by staff users and the first edit which is queried as RECEIVED for some reason - staff_users = User.objects.all().filter(is_staff=True).values_list("id", flat=True) + # Exclude any non-human users + users = User.objects.exclude( + username__icontains="ahjorestapi", + first_name__exact="", + last_name__exact="", + ).values_list("id", flat=True) + application_history = ( application.history.filter( - history_user_id__in=list(staff_users), + history_user__id__in=users, status__in=[ ApplicationStatus.HANDLING, + ApplicationStatus.ADDITIONAL_INFORMATION_NEEDED, ], ) | application.history.filter(status=ApplicationStatus.RECEIVED)[:1] @@ -323,37 +179,20 @@ def get_application_change_history_made_by_handler(application: Application) -> attachment_diffs = [] for attachment in application.attachments.all(): for new_record in attachment.history.filter( - history_type="+", history_date__gte=submitted_at + history_type="+", + history_date__gte=submitted_at, + history_user_id__in=list(users), ): change_set_base = _get_change_set_base(new_record) change_set_base["changes"] = [ { "field": "attachments", "old": "+", - "new": f"{new_record.attachment_file} ({_(new_record.attachment_type)})", + "new": new_record.attachment_file, + "meta": new_record.attachment_type, } ] attachment_diffs.append(change_set_base) - change_sets += attachment_diffs - + change_sets = change_sets + attachment_diffs + change_sets.sort(key=lambda x: x["date"], reverse=True) return change_sets - - -def get_application_change_history_for_applicant_from_audit_log( - application: Application, -) -> list: - """ - Get all changes to application that is made by handlers. Audit log based solution. - As the audit log doesn't contain changes to related models, this is mostly useless. - Maybe this can be used later when the audit log is fixed. Remove if you want. - """ - handler_user_ids = _get_handlers(as_ids=True) - changes = [] - for log_entry in ( - AuditLogEntry.objects.filter(message__audit_event__operation="UPDATE") - .filter(message__audit_event__target__id=str(application.id)) - .filter(message__audit_event__target__changes__isnull=False) - .filter(message__audit_event__actor__user_id__in=handler_user_ids) - ): - changes += log_entry.message["audit_event"]["target"]["changes"] - return changes diff --git a/backend/benefit/applications/tests/test_application_change_sets.py b/backend/benefit/applications/tests/test_application_change_sets.py new file mode 100755 index 0000000000..578822000c --- /dev/null +++ b/backend/benefit/applications/tests/test_application_change_sets.py @@ -0,0 +1,269 @@ +from datetime import timedelta +from unittest import mock + +import faker +from freezegun import freeze_time +from rest_framework.reverse import reverse + +from applications.api.v1.serializers.application import ( + ApplicantApplicationSerializer, + HandlerApplicationSerializer, +) +from applications.enums import ApplicationActions, ApplicationStatus, AttachmentType +from applications.tests.conftest import * # noqa +from applications.tests.test_applications_api import ( + _upload_pdf, + add_attachments_to_application, +) +from helsinkibenefit.tests.conftest import * # noqa +from terms.tests.conftest import * # noqa + + +def get_handler_detail_url(application): + return reverse("v1:handler-application-detail", kwargs={"pk": application.id}) + + +def get_applicant_detail_url(application): + return reverse("v1:applicant-application-detail", kwargs={"pk": application.id}) + + +def _flatten_dict(d, parent_key="", sep="."): + items = [] + for k, v in d.items(): + new_key = f"{parent_key}{sep}{k}" if parent_key else k + if isinstance(v, dict): + items.extend(_flatten_dict(v, new_key, sep=sep).items()) + else: + items.append((new_key, v)) + return dict(items) + + +handler_edit_payloads = [ + { + "change_reason": "Change employee first & last name, company contact's phone number", + "company_contact_person_phone_number": "+35850000000", + "employee": { + "first_name": "Firstname1", + "last_name": "Lastname1", + }, + }, + { + "change_reason": "Change employee last name and company contact's phone number", + "company_contact_person_phone_number": "+35850000001", + "employee": { + "last_name": "Lastname2", + }, + }, + { + "change_reason": "Change employee last name", + "employee": { + "last_name": faker.Faker().last_name(), + }, + }, + { + "change_reason": "Edit many many fields", + "employee": { + "first_name": "Cool", + "last_name": "Kanerva", + "social_security_number": "211081-2043", + "employee_language": "sv", + "job_title": "Some-asiantuntija", + "monthly_pay": 1111, + "vacation_money": 222, + "other_expenses": 333, + "working_hours": 18.0, + "collective_bargaining_agreement": "TES", + "birthday": "1987-01-01", + }, + "company": { + "street_address": "Maasalontie 952", + "postcode": "80947", + "city": "Haapavesi", + "bank_account_number": "FI7600959247562223", + }, + "official_company_street_address": "Maasalontie 952", + "official_company_city": "Haapavesi", + "official_company_postcode": "80947", + "use_alternative_address": False, + "company_bank_account_number": "FI6547128581000605", + "company_contact_person_first_name": "Malla", + "company_contact_person_last_name": "Jout-Sen", + "company_contact_person_phone_number": "+358401234567", + "company_contact_person_email": "yjdh-helsinkilisa@example.net", + "applicant_language": "fi", + "co_operation_negotiations": True, + "co_operation_negotiations_description": "Aenean fringilla lorem tellus", + "additional_pay_subsidy_percent": None, + "apprenticeship_program": None, + "start_date": "2024-01-01", + "end_date": "2024-02-02", + }, + {"change_reason": None, "status": ApplicationStatus.ADDITIONAL_INFORMATION_NEEDED}, +] + + +applicant_edit_payloads = [ + { + "change_reason": None, + "company_contact_person_first_name": "Tette", + "company_contact_person_last_name": "Tötterström", + "company_contact_person_phone_number": "+358501234567", + "company_contact_person_email": "yjdh@example.net", + "applicant_language": "en", + "co_operation_negotiations": False, + "co_operation_negotiations_description": "", + }, + { + "change_reason": None, + "status": ApplicationStatus.HANDLING, + "employee": { + "first_name": "Aura", + "last_name": "Muumaamustikka", + "employee_language": "en", + "job_title": "Metsän henki", + "monthly_pay": 1234, + "vacation_money": 321, + "other_expenses": 313, + "working_hours": 33.0, + "collective_bargaining_agreement": "JES", + "birthday": "2008-01-01", + }, + "start_date": "2023-12-01", + "end_date": "2024-01-02", + }, +] + + +def compare_fields(edit_payloads, changes): + # Assert that each field change exist in change sets + for i, expected_change in enumerate(edit_payloads): + assert changes[i]["reason"] == expected_change["change_reason"] + expected_change.pop("change_reason") + + expected_fields = dict(_flatten_dict(expected_change)) + + changed_fields = { + change["field"]: change["new"] for change in changes[i]["changes"] + } + + for key in changed_fields: + assert ( + str(expected_fields[key]) == str(changed_fields[key]) + if isinstance(expected_fields[key], str) + else float(expected_fields[key]) == float(changed_fields[key]) + ) + + +def check_handler_changes(handler_edit_payloads, changes): + # Reverse the payloads to match the order of the changes + handler_edit_payloads.reverse() + + # Add a mock row which gets inserted when application status changes to "handling" + handler_edit_payloads.append({"change_reason": None, "handler": "Unknown user"}) + handler_edit_payloads.append( + {"change_reason": None, "status": ApplicationStatus.HANDLING} + ) + + compare_fields(handler_edit_payloads, changes) + assert len(changes) == len(handler_edit_payloads) + + +def check_applicant_changes(applicant_edit_payloads, changes, application): + # Reverse the payloads to match the order of the changes + applicant_edit_payloads.reverse() + applicant_edit_payloads.append( + { + "change_reason": None, + "attachments": application.attachments.last().attachment_file.name, + } + ) + + compare_fields(applicant_edit_payloads, changes) + + assert len(changes) == len(applicant_edit_payloads) + + +def test_application_history_change_sets( + request, handler_api_client, api_client, application +): + payload = HandlerApplicationSerializer(application).data + payload["status"] = ApplicationStatus.RECEIVED + with mock.patch( + "terms.models.ApplicantTermsApproval.terms_approval_needed", return_value=False + ): + add_attachments_to_application(request, application) + response = handler_api_client.put( + get_handler_detail_url(application), + payload, + ) + + assert response.status_code == 200 + + application.refresh_from_db() + payload = HandlerApplicationSerializer(application).data + payload["status"] = ApplicationStatus.HANDLING + response = handler_api_client.put( + get_handler_detail_url(application), + payload, + ) + assert response.status_code == 200 + + # Set up the handler edits + def update_handler_application(application_payload, frozen_datetime): + frozen_datetime.tick(delta=timedelta(seconds=1)) + application.refresh_from_db() + payload = HandlerApplicationSerializer(application).data + payload["action"] = ApplicationActions.HANDLER_ALLOW_APPLICATION_EDIT + response = handler_api_client.put( + get_handler_detail_url(application), + {**payload, **application_payload}, + ) + assert response.status_code == 200 + return response + + # Set up the applicant edits + def update_applicant_application(application_payload, frozen_datetime): + frozen_datetime.tick(delta=timedelta(seconds=1)) + application.refresh_from_db() + payload = ApplicantApplicationSerializer(application).data + response = api_client.put( + get_applicant_detail_url(application), + {**payload, **application_payload}, + ) + assert response.status_code == 200 + return response + + with freeze_time("2024-01-01") as frozen_datetime: + for application_payload in handler_edit_payloads: + response = update_handler_application(application_payload, frozen_datetime) + + changes = response.data["changes"] + check_handler_changes(handler_edit_payloads, changes) + with freeze_time("2024-01-02") as frozen_datetime: + with mock.patch( + "terms.models.ApplicantTermsApproval.terms_approval_needed", + return_value=False, + ): + # add the required attachments except consent + response = _upload_pdf( + request, + api_client, + application, + attachment_type=AttachmentType.HELSINKI_BENEFIT_VOUCHER, + ) + assert response.status_code == 201 + + for application_payload in applicant_edit_payloads: + response = update_applicant_application( + application_payload, frozen_datetime + ) + + changes = handler_api_client.get(get_handler_detail_url(application)).data[ + "changes" + ] + + # Just split from the head of changes - we don't want to check handler's changes again + # Plus one for the attachment change + applicant_changes = changes[0 : len(applicant_edit_payloads) + 1] + + check_applicant_changes(applicant_edit_payloads, applicant_changes, application) diff --git a/backend/benefit/applications/tests/test_applications_api.py b/backend/benefit/applications/tests/test_applications_api.py index 7c58600e24..c330d91e6a 100755 --- a/backend/benefit/applications/tests/test_applications_api.py +++ b/backend/benefit/applications/tests/test_applications_api.py @@ -4,7 +4,7 @@ import re import tempfile import uuid -from datetime import date, datetime, timedelta, timezone +from datetime import date, datetime, timezone from decimal import Decimal from unittest import mock @@ -28,7 +28,6 @@ ) from applications.enums import ( AhjoStatus, - ApplicationActions, ApplicationAlterationState, ApplicationBatchStatus, ApplicationStatus, @@ -2385,164 +2384,6 @@ def test_application_pdf_print_denied(api_client, anonymous_client): assert response.status_code == 403 -def _flatten_dict(d, parent_key="", sep="."): - items = [] - for k, v in d.items(): - new_key = f"{parent_key}{sep}{k}" if parent_key else k - if isinstance(v, dict): - items.extend(_flatten_dict(v, new_key, sep=sep).items()) - else: - items.append((new_key, v)) - return dict(items) - - -def test_application_history_change_sets_for_handler( - request, handler_api_client, application -): - # Setup application to handling status - with freeze_time("2021-01-01") as frozen_datetime: - add_attachments_to_application(request, application) - - payload = HandlerApplicationSerializer(application).data - payload["status"] = ApplicationStatus.RECEIVED - with mock.patch( - "terms.models.ApplicantTermsApproval.terms_approval_needed", return_value=False - ): - response = handler_api_client.put( - get_handler_detail_url(application), - payload, - ) - assert response.status_code == 200 - - application.refresh_from_db() - payload = HandlerApplicationSerializer(application).data - payload["status"] = ApplicationStatus.HANDLING - response = handler_api_client.put( - get_handler_detail_url(application), - payload, - ) - assert response.status_code == 200 - - # Mock the actual handler edits - with freeze_time("2024-01-01") as frozen_datetime: - update_payloads = [ - { - "change_reason": "Change employee first & last name, company contact's phone number", - "company_contact_person_phone_number": "+35850000000", - "employee": { - "first_name": "Firstname1", - "last_name": "Lastname1", - }, - }, - { - "change_reason": "Change employee last name and company contact's phone number", - "company_contact_person_phone_number": "+35850000001", - "employee": { - "last_name": "Lastname2", - }, - }, - { - "change_reason": "Change employee last name", - "employee": { - "last_name": faker.Faker().last_name(), - }, - }, - { - "change_reason": "Edit many many fields", - "employee": { - "first_name": "Cool", - "last_name": "Kanerva", - "social_security_number": "211081-2043", - "employee_language": "sv", - "job_title": "Some-asiantuntija", - "monthly_pay": 1111, - "vacation_money": 222, - "other_expenses": 333, - "working_hours": 18.0, - "collective_bargaining_agreement": "TES", - "birthday": "1987-01-01", - }, - "company": { - "street_address": "Maasalontie 952", - "postcode": "80947", - "city": "Haapavesi", - "bank_account_number": "FI7600959247562223", - }, - "official_company_street_address": "Maasalontie 952", - "official_company_city": "Haapavesi", - "official_company_postcode": "80947", - "use_alternative_address": False, - "company_bank_account_number": "FI6547128581000605", - "company_contact_person_first_name": "Malla", - "company_contact_person_last_name": "Jout-Sen", - "company_contact_person_phone_number": "+358401234567", - "company_contact_person_email": "yjdh-helsinkilisa@example.net", - "applicant_language": "fi", - "co_operation_negotiations": True, - "co_operation_negotiations_description": "Aenean fringilla lorem tellus", - "additional_pay_subsidy_percent": None, - "apprenticeship_program": None, - "start_date": "2024-01-01", - "end_date": "2024-02-02", - }, - ] - - def update_application(application_payload): - frozen_datetime.tick(delta=timedelta(seconds=1)) - application.refresh_from_db() - payload = HandlerApplicationSerializer(application).data - payload["action"] = ApplicationActions.HANDLER_ALLOW_APPLICATION_EDIT - response = handler_api_client.put( - get_handler_detail_url(application), - {**payload, **application_payload}, - ) - assert response.status_code == 200 - return response - - response = None - for application_payload in update_payloads: - response = update_application(application_payload) - - changes = response.data["changes"]["handler"] - - update_payloads.reverse() - - # Add a mock row which gets inserted when application status changes to "handling" - update_payloads.append({"change_reason": None, "handler": "Unknown user"}) - - assert len(changes) == len(update_payloads) - - # Assert that each field change exist in change sets - for i, row in enumerate(update_payloads): - assert changes[i]["reason"] == row["change_reason"] - row.pop("change_reason") - - input_fields = dict(_flatten_dict(row)) - - changed_fields = { - change["field"]: change["new"] for change in changes[i]["changes"] - } - - for key in changed_fields: - assert ( - str(input_fields[key]) == str(changed_fields[key]) - if isinstance(input_fields[key], str) - else float(input_fields[key]) == float(changed_fields[key]) - ) - - -def test_application_handler_change(api_client, handler_api_client, application): - response = api_client.patch( - reverse("v1:handler-application-change-handler", kwargs={"pk": application.id}), - ) - assert response.status_code == 403 - - response = handler_api_client.patch( - reverse("v1:handler-application-change-handler", kwargs={"pk": application.id}), - ) - assert response.status_code == 200 - - def test_application_alterations(api_client, handler_api_client, application): cancelled_alteration = _create_application_alteration(application) cancelled_alteration.state = ApplicationAlterationState.CANCELLED diff --git a/frontend/benefit/handler/public/locales/en/common.json b/frontend/benefit/handler/public/locales/en/common.json index abbcf21b63..bc503300d1 100644 --- a/frontend/benefit/handler/public/locales/en/common.json +++ b/frontend/benefit/handler/public/locales/en/common.json @@ -644,7 +644,8 @@ }, "changeReason": { "label": "Selvitys", - "placeholder": "Syy muokkaukseen" + "placeholder": "Syy muokkaukseen", + "additionalInformationRequired": "Lisätietoja vaadittu" } }, "salaryExpensesExplanation": "Ilmoita bruttopalkka, sivukulut ja lomaraha euroina kuukaudessa.", @@ -1835,6 +1836,9 @@ }, "companyForm": { "label": "Yritysmuoto" + }, + "status": { + "label": "Tila" } } } diff --git a/frontend/benefit/handler/public/locales/fi/common.json b/frontend/benefit/handler/public/locales/fi/common.json index 24c4883402..03a9fc0f42 100644 --- a/frontend/benefit/handler/public/locales/fi/common.json +++ b/frontend/benefit/handler/public/locales/fi/common.json @@ -644,7 +644,8 @@ }, "changeReason": { "label": "Selvitys", - "placeholder": "Syy muokkaukseen" + "placeholder": "Syy muokkaukseen", + "additionalInformationRequired": "Lisätietoja vaadittu" } }, "salaryExpensesExplanation": "Ilmoita bruttopalkka, sivukulut ja lomaraha euroina kuukaudessa.", @@ -1834,6 +1835,9 @@ }, "companyForm": { "label": "Yritysmuoto" + }, + "status": { + "label": "Tila" } } } diff --git a/frontend/benefit/handler/public/locales/sv/common.json b/frontend/benefit/handler/public/locales/sv/common.json index abbcf21b63..bc503300d1 100644 --- a/frontend/benefit/handler/public/locales/sv/common.json +++ b/frontend/benefit/handler/public/locales/sv/common.json @@ -644,7 +644,8 @@ }, "changeReason": { "label": "Selvitys", - "placeholder": "Syy muokkaukseen" + "placeholder": "Syy muokkaukseen", + "additionalInformationRequired": "Lisätietoja vaadittu" } }, "salaryExpensesExplanation": "Ilmoita bruttopalkka, sivukulut ja lomaraha euroina kuukaudessa.", @@ -1835,6 +1836,9 @@ }, "companyForm": { "label": "Yritysmuoto" + }, + "status": { + "label": "Tila" } } } diff --git a/frontend/benefit/handler/src/components/applicationForm/reviewChanges/ReviewEditChanges.sc.ts b/frontend/benefit/handler/src/components/applicationForm/reviewChanges/ReviewEditChanges.sc.ts index 95f2584685..4c650fc09d 100644 --- a/frontend/benefit/handler/src/components/applicationForm/reviewChanges/ReviewEditChanges.sc.ts +++ b/frontend/benefit/handler/src/components/applicationForm/reviewChanges/ReviewEditChanges.sc.ts @@ -14,11 +14,13 @@ export const $ChangeRowValue = styled.dd<$ChangeRowValueProps>` svg { margin: 0 ${(props) => props.theme.spacing.xs3}; width: 18px; + min-width: 18px; height: 18px; + min-height: 18px; } span { - white-space: pre; + white-space: normal; } `; @@ -26,8 +28,14 @@ export const $ChangeRowLabel = styled.dt` margin: 0; `; -export const $ChangeSet = styled.div` - background: ${(props) => props.theme.colors.silverLight}; +type $ChangeSetProps = { + isChangeByStaff: boolean; +}; +export const $ChangeSet = styled.div<$ChangeSetProps>` + background: ${(props) => + props.isChangeByStaff + ? props.theme.colors.silverLight + : props.theme.colors.fogLight}; padding: ${(props) => props.theme.spacing.s}; font-size: 0.99em; margin-bottom: ${(props) => props.theme.spacing.xs}; diff --git a/frontend/benefit/handler/src/components/applicationForm/reviewChanges/utils.ts b/frontend/benefit/handler/src/components/applicationForm/reviewChanges/utils.ts index 126c14fe6d..60e7fb6874 100644 --- a/frontend/benefit/handler/src/components/applicationForm/reviewChanges/utils.ts +++ b/frontend/benefit/handler/src/components/applicationForm/reviewChanges/utils.ts @@ -1,5 +1,10 @@ import { APPLICATION_FIELD_KEYS } from 'benefit/handler/constants'; -import { EMPLOYEE_KEYS, PAY_SUBSIDY_GRANTED } from 'benefit-shared/constants'; +import { + APPLICATION_STATUSES, + ATTACHMENT_TYPES, + EMPLOYEE_KEYS, + PAY_SUBSIDY_GRANTED, +} from 'benefit-shared/constants'; import { DeMinimisAid } from 'benefit-shared/types/application'; import { formatIBAN } from 'benefit-shared/utils/common'; import camelCase from 'lodash/camelCase'; @@ -57,6 +62,20 @@ export const formatOrTranslateValue = ( )}` ); } + + if ( + [ + APPLICATION_STATUSES.HANDLING, + APPLICATION_STATUSES.RECEIVED, + APPLICATION_STATUSES.INFO_REQUIRED, + APPLICATION_STATUSES.ACCEPTED, + APPLICATION_STATUSES.REJECTED, + APPLICATION_STATUSES.CANCELLED, + ].includes(value as APPLICATION_STATUSES) + ) { + return t(`common:applications.list.columns.applicationStatuses.${value}`); + } + return value; } @@ -117,3 +136,27 @@ export const getDiffPrefilter = ( export const prepareChangeFieldName = (fieldName: string): string => camelCase(fieldName.replace('employee', '')); + +export const translateChangeFieldMeta = ( + t: TFunction, + meta: string +): string => { + const attachmentTypes = [ + ATTACHMENT_TYPES.EMPLOYMENT_CONTRACT, + ATTACHMENT_TYPES.PAY_SUBSIDY_CONTRACT, + ATTACHMENT_TYPES.EDUCATION_CONTRACT, + ATTACHMENT_TYPES.HELSINKI_BENEFIT_VOUCHER, + ATTACHMENT_TYPES.EMPLOYEE_CONSENT, + ATTACHMENT_TYPES.FULL_APPLICATION, + ATTACHMENT_TYPES.OTHER_ATTACHMENT, + // eslint-disable-next-line unicorn/no-array-callback-reference + ].map(camelCase); + + // eslint-disable-next-line security/detect-non-literal-regexp + const regexp = new RegExp(attachmentTypes.join('|'), 'g'); + const key = camelCase(meta); + return key.replace( + regexp, + t(`common:applications.sections.attachments.types.${key}.title`) + ); +}; diff --git a/frontend/benefit/handler/src/components/sidebar/ChangeList.tsx b/frontend/benefit/handler/src/components/sidebar/ChangeList.tsx index 24d673867f..bef0656592 100644 --- a/frontend/benefit/handler/src/components/sidebar/ChangeList.tsx +++ b/frontend/benefit/handler/src/components/sidebar/ChangeList.tsx @@ -1,4 +1,3 @@ -import { ApplicationChangesData } from 'benefit/handler/types/application'; import { ChangeListData } from 'benefit/handler/types/changes'; import { IconHistory } from 'hds-react'; import orderBy from 'lodash/orderBy'; @@ -14,7 +13,7 @@ import { convertToUIDateFormat } from 'shared/utils/date.utils'; import ChangeSet from './ChangeSet'; type ChangeListProps = { - data: ApplicationChangesData; + data: ChangeListData[]; }; const previousDate = ( @@ -32,11 +31,10 @@ const doesPreviousDateMatch = ( convertToUIDateFormat(previousDate(combinedAndOrderedChangeSets, index)); const ChangeList: React.FC = ({ data }: ChangeListProps) => { - const { handler, applicant } = data; - const combined: ChangeListData[] = [...handler, ...applicant]; - const combinedAndOrderedChangeSets = orderBy(combined, ['date'], ['desc']); + const orderedData = orderBy(data, ['date'], ['desc']); const { t } = useTranslation(); - if (combinedAndOrderedChangeSets.length === 0) { + + if (orderedData.length === 0) { return ( <$MessagesList variant="message"> <$Empty> @@ -49,15 +47,13 @@ const ChangeList: React.FC = ({ data }: ChangeListProps) => { return ( <$Actions> - {combinedAndOrderedChangeSets.map((changeSet, index) => ( + {orderedData.map((changeSet, index) => ( - {!doesPreviousDateMatch( - changeSet, - combinedAndOrderedChangeSets, - index - ) &&

{convertToUIDateFormat(changeSet.date)}

} + {!doesPreviousDateMatch(changeSet, orderedData, index) && ( +

{convertToUIDateFormat(changeSet.date)}

+ )}
))} diff --git a/frontend/benefit/handler/src/components/sidebar/ChangeListApplicant.tsx b/frontend/benefit/handler/src/components/sidebar/ChangeListApplicant.tsx deleted file mode 100644 index fc31896c3f..0000000000 --- a/frontend/benefit/handler/src/components/sidebar/ChangeListApplicant.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { ChangeData, ChangeListData } from 'benefit/handler/types/changes'; -import { IconArrowRight } from 'hds-react'; -import { useTranslation } from 'next-i18next'; -import * as React from 'react'; -import { $Actions } from 'shared/components/messaging/Messaging.sc'; -import { convertToUIDateAndTimeFormat } from 'shared/utils/date.utils'; - -import { $ViewFieldBold } from '../applicationForm/ApplicationForm.sc'; -import { - $ChangeRowLabel, - $ChangeRowValue, - $ChangeSet, - $ChangeSetHeader, -} from '../applicationForm/reviewChanges/ReviewEditChanges.sc'; -import { - formatOrTranslateValue, - prepareChangeFieldName, - translateLabelFromPath, -} from '../applicationForm/reviewChanges/utils'; - -type ChangeListApplicantProps = { - data: ChangeListData; -}; - -const ChangesByApplicant: React.FC = ({ - data, -}: ChangeListApplicantProps) => { - const { t } = useTranslation(); - const { date, user, changes } = data; - - return ( - <$Actions> - <$ChangeSet as="dl"> - <$ChangeSetHeader> - {user} - {convertToUIDateAndTimeFormat(date)} - - {changes?.map((change: ChangeData) => ( - - <$ChangeRowLabel> - <$ViewFieldBold> - {translateLabelFromPath( - t, - prepareChangeFieldName(change.field).split('.') - )} - - - <$ChangeRowValue> - - {formatOrTranslateValue( - t, - change.old, - prepareChangeFieldName(change.field) - )} - - - - {formatOrTranslateValue( - t, - change.new, - prepareChangeFieldName(change.field) - )} - - - - ))} - - - ); -}; - -export default ChangesByApplicant; diff --git a/frontend/benefit/handler/src/components/sidebar/ChangeSet.tsx b/frontend/benefit/handler/src/components/sidebar/ChangeSet.tsx index f5ed14ecc4..acd883617e 100644 --- a/frontend/benefit/handler/src/components/sidebar/ChangeSet.tsx +++ b/frontend/benefit/handler/src/components/sidebar/ChangeSet.tsx @@ -18,6 +18,7 @@ import { import { formatOrTranslateValue, prepareChangeFieldName, + translateChangeFieldMeta, translateLabelFromPath, } from '../applicationForm/reviewChanges/utils'; @@ -44,15 +45,15 @@ const ChangeSet: React.FC = ({ data }: ChangeSetProps) => { ); return ( - <$ChangeSet> + <$ChangeSet isChangeByStaff={user.staff}> <$ChangeSetHeader> - {user} + {user.name} {convertToUIDateAndTimeFormat(date)}
{changes.map((change: ChangeData) => ( <$ChangeRowLabel> <$ViewFieldBold> @@ -85,6 +86,9 @@ const ChangeSet: React.FC = ({ data }: ChangeSetProps) => { change.new, prepareChangeFieldName(change.field) )} + {change.meta + ? ` (${translateChangeFieldMeta(t, change.meta)})` + : null} @@ -105,7 +109,11 @@ const ChangeSet: React.FC = ({ data }: ChangeSetProps) => { <$ViewField> {isAttachmentChange ? t('common:changes.fields.attachments.newAttachment') - : formatOrTranslateValue(t, reason)} + : user.staff + ? formatOrTranslateValue(t, reason) + : t( + 'common:applications.sections.fields.changeReason.additionalInformationRequired' + )} diff --git a/frontend/benefit/handler/src/types/application.d.ts b/frontend/benefit/handler/src/types/application.d.ts index 4461a66fc9..5ae6fac517 100644 --- a/frontend/benefit/handler/src/types/application.d.ts +++ b/frontend/benefit/handler/src/types/application.d.ts @@ -118,11 +118,6 @@ export type HandledAplication = { decisionMakerId?: string; }; -export type ApplicationChangesData = { - handler: ChangeListData[]; - applicant: ChangeListData[]; -}; - // Handler application export type Application = { @@ -157,7 +152,7 @@ export type Application = { totalDeminimisAmount?: string; action?: APPLICATION_ACTIONS; changeReason?: string; - changes?: ApplicationChangesData; + changes?: ChangeListData[]; decisionProposalDraft?: DecisionProposalDraft; handler?: User; } & ApplicationForm; diff --git a/frontend/benefit/handler/src/types/changes.d.ts b/frontend/benefit/handler/src/types/changes.d.ts index 776d3cc5b2..86a15adfa0 100644 --- a/frontend/benefit/handler/src/types/changes.d.ts +++ b/frontend/benefit/handler/src/types/changes.d.ts @@ -2,11 +2,17 @@ export type ChangeData = { old: string; new: string; field: string; + meta?: string; +}; + +type User = { + name: string; + staff: boolean; }; export type ChangeListData = { changes: ChangeData[]; reason: string; - user: string; + user: User; date: string; };