diff --git a/backend/benefit/applications/api/v1/instalment_views.py b/backend/benefit/applications/api/v1/instalment_views.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/backend/benefit/applications/api/v1/serializers/application.py b/backend/benefit/applications/api/v1/serializers/application.py index 351857c72c..47530b7d33 100755 --- a/backend/benefit/applications/api/v1/serializers/application.py +++ b/backend/benefit/applications/api/v1/serializers/application.py @@ -62,9 +62,11 @@ from calculator.api.v1.serializers import ( CalculationSearchSerializer, CalculationSerializer, + InstalmentSerializer, PaySubsidySerializer, TrainingCompensationSerializer, ) +from calculator.enums import InstalmentStatus from calculator.models import Calculation from common.delay_call import call_now_or_later, do_delayed_calls_at_end from common.exceptions import BenefitAPIException @@ -1903,6 +1905,7 @@ class Meta: "batch", "ahjo_error", "talpa_status", + "pending_instalment", ] read_only_fields = [ @@ -1927,6 +1930,7 @@ class Meta: "batch", "ahjo_error", "talpa_status", + "pending_instalment", ] archived = serializers.BooleanField() @@ -1947,6 +1951,26 @@ class Meta: "Timestamp when the application was handled (accepted/rejected/cancelled)" ), ) + pending_instalment = serializers.SerializerMethodField("get_pending_instalment") + + def get_pending_instalment(self, application): + """Get the latest pending instalment for the application""" + try: + instalments = application.calculation.instalments.filter( + instalment_number__gt=1 + ) + instalment = ( + instalments.exclude(status=InstalmentStatus.COMPLETED) + .order_by("-due_date") + .first() + or None + ) + if instalment is not None: + return InstalmentSerializer(instalment).data + except AttributeError: + return None + return None + ahjo_error = serializers.SerializerMethodField("get_latest_ahjo_error") def get_latest_ahjo_error(self, obj) -> Union[Dict, None]: diff --git a/backend/benefit/applications/fixtures/test_applications.json b/backend/benefit/applications/fixtures/test_applications.json index 5b77064d86..cb85b66647 100644 --- a/backend/benefit/applications/fixtures/test_applications.json +++ b/backend/benefit/applications/fixtures/test_applications.json @@ -1426,5 +1426,354 @@ "end_date": null, "description_type": null } + }, + { + "model": "applications.application", + "pk": "10c25d67-f783-4625-9ff4-2418b629f20a", + "fields": { + "created_at": "2024-11-04T10:06:33.596Z", + "modified_at": "2024-11-04T10:10:12.337Z", + "company": "746afc66-6f5a-4cb4-805f-4b58380b4745", + "status": "accepted", + "talpa_status": "not_sent_to_talpa", + "application_origin": "applicant", + "application_number": 125000, + "company_name": "Demo I. Haanpää Oy", + "company_form": "OY", + "company_form_code": 16, + "company_department": "", + "official_company_street_address": "Vasaratie 4 A 3", + "official_company_city": "Vaasa", + "official_company_postcode": "65350", + "use_alternative_address": false, + "alternative_company_street_address": "", + "alternative_company_city": "", + "alternative_company_postcode": "", + "company_bank_account_number": "FI6033556370003404", + "company_contact_person_first_name": "Neo", + "company_contact_person_last_name": "Nönnönnöö", + "company_contact_person_phone_number": "+358501234", + "company_contact_person_email": "heips@example.com", + "association_has_business_activities": null, + "applicant_language": "fi", + "association_immediate_manager_check": null, + "co_operation_negotiations": false, + "co_operation_negotiations_description": "", + "pay_subsidy_granted": "not_granted", + "pay_subsidy_percent": null, + "additional_pay_subsidy_percent": null, + "apprenticeship_program": null, + "archived": false, + "application_step": "step_6", + "benefit_type": "salary_benefit", + "start_date": "2024-01-01", + "end_date": "2025-01-01", + "paper_application_date": null, + "de_minimis_aid": false, + "batch": "fb9e81a4-cf6d-4f7f-abee-366c8a5bfbfc", + "ahjo_case_id": null, + "ahjo_case_guid": null, + "handled_by_ahjo_automation": true, + "handler": "47ecedfa-351b-4815-bfac-96bdbc640178", + "bases": [] + } + }, + { + "model": "applications.employee", + "pk": "389c9dbc-5030-410b-8ce7-08a7351c60bf", + "fields": { + "created_at": "2024-11-04T10:06:33.606Z", + "modified_at": "2024-11-04T10:10:12.344Z", + "application": "10c25d67-f783-4625-9ff4-2418b629f20a", + "encrypted_first_name": "Juno", + "encrypted_last_name": "Yucca-Palmu", + "first_name": "Juno", + "last_name": "Yucca-Palmu", + "encrypted_social_security_number": "111111A111C", + "social_security_number": "111111A111C", + "phone_number": "", + "email": "", + "employee_language": "fi", + "job_title": "Taittaja", + "monthly_pay": "9999.00", + "vacation_money": "9999.00", + "other_expenses": "9999.00", + "working_hours": "32.00", + "collective_bargaining_agreement": "MEH", + "is_living_in_helsinki": true, + "commission_amount": null, + "commission_description": "" + } + }, + + { + "model": "applications.attachment", + "pk": "3a6fea3c-fbd6-4f04-a4b7-d9f9dcad801c", + "fields": { + "created_at": "2024-11-04T10:09:27.403Z", + "modified_at": "2024-11-04T10:09:27.403Z", + "application": "10c25d67-f783-4625-9ff4-2418b629f20a", + "attachment_type": "employee_consent", + "content_type": "image/png", + "attachment_file": "1_6K4Jtln.png", + "ahjo_version_series_id": null, + "ahjo_hash_value": null, + "downloaded_by_ahjo": null + } + }, + { + "model": "applications.attachment", + "pk": "a44c7b72-991e-4c21-b9de-237fad2525a5", + "fields": { + "created_at": "2024-11-04T10:09:18.013Z", + "modified_at": "2024-11-04T10:09:18.013Z", + "application": "10c25d67-f783-4625-9ff4-2418b629f20a", + "attachment_type": "employment_contract", + "content_type": "image/png", + "attachment_file": "1.png", + "ahjo_version_series_id": null, + "ahjo_hash_value": null, + "downloaded_by_ahjo": null + } + }, + { + "model": "applications.ahjostatus", + "pk": 1, + "fields": { + "created_at": "2024-11-04T10:10:12.358Z", + "modified_at": "2024-11-04T10:10:12.358Z", + "status": "submitted_but_not_sent_to_ahjo", + "application": "10c25d67-f783-4625-9ff4-2418b629f20a", + "error_from_ahjo": null, + "ahjo_request_id": null, + "validation_error_from_ahjo": null + } + }, + { + "model": "applications.ahjodecisionproposaldraft", + "pk": 1, + "fields": { + "created_at": "2024-11-04T10:06:33.611Z", + "modified_at": "2024-11-04T10:06:33.611Z", + "application": "10c25d67-f783-4625-9ff4-2418b629f20a", + "review_step": "1", + "status": null, + "log_entry_comment": null, + "granted_as_de_minimis_aid": false, + "handler_role": null, + "decision_text": null, + "justification_text": null, + "decision_maker_name": null, + "decision_maker_id": null + } + }, + { + "model": "applications.applicationlogentry", + "pk": "595dd88c-e8fb-466a-aec9-e64bbf9f122c", + "fields": { + "created_at": "2024-11-04T10:06:35.596Z", + "modified_at": "2024-11-04T10:06:35.596Z", + "application": "10c25d67-f783-4625-9ff4-2418b629f20a", + "from_status": "draft", + "to_status": "received", + "comment": "" + } + }, + { + "model": "calculator.calculation", + "pk": "aa52e56a-3d64-4129-ae7d-423585d47766", + "fields": { + "created_at": "2024-11-04T10:10:12.357Z", + "modified_at": "2024-11-04T11:20:46.842Z", + "handler": "47ecedfa-351b-4815-bfac-96bdbc640178", + "application": "10c25d67-f783-4625-9ff4-2418b629f20a", + "monthly_pay": "9999.00", + "vacation_money": "9999.00", + "other_expenses": "9999.00", + "start_date": "2024-01-01", + "end_date": "2025-03-01", + "state_aid_max_percentage": 100, + "calculated_benefit_amount": "11224.00", + "override_monthly_benefit_amount": null, + "granted_as_de_minimis_aid": false, + "target_group_check": false, + "override_monthly_benefit_amount_comment": "" + } + }, + { + "model": "applications.applicationbatch", + "pk": "fb9e81a4-cf6d-4f7f-abee-366c8a5bfbfc", + "fields": { + "created_at": "2024-06-13T14:02:03.208Z", + "modified_at": "2024-06-13T14:02:13.212Z", + "handler": "47ecedfa-351b-4815-bfac-96bdbc640178", + "status": "accepted", + "proposal_for_decision": "accepted", + "decision_maker_title": "Päätöksentekijä", + "decision_maker_name": "Malli Päättäjä", + "section_of_the_law": "§123", + "decision_date": "2024-11-04", + "p2p_inspector_name": null, + "p2p_inspector_email": null, + "p2p_checker_name": null, + "expert_inspector_name": "Malli Tarkastaja", + "expert_inspector_email": "malli.tarkastaja@example.com", + "expert_inspector_title": "Tarkastaja", + "auto_generated_by_ahjo": false + } + }, + { + "model": "calculator.paysubsidy", + "pk": "fb1bd224-4a01-45ad-a95c-2b0cc2cc07a4", + "fields": { + "created_at": "2024-06-13T13:43:35.578Z", + "modified_at": "2024-06-13T13:50:58.054Z", + "application": "3544c528-73cc-4a08-8240-09f713a14990", + "ordering": 0, + "start_date": "2024-06-17", + "end_date": "2024-08-14", + "pay_subsidy_percent": 50, + "work_time_percent": "65.00", + "disability_or_illness": false + } + }, + { + "model": "calculator.instalment", + "pk": "ed9ab8ee-6316-4557-b34c-6ded42504057", + "fields": { + "created_at": "2024-11-04T11:20:46.842Z", + "modified_at": "2024-11-04T11:20:46.842Z", + "calculation": "aa52e56a-3d64-4129-ae7d-423585d47766", + "instalment_number": 1, + "amount": "9600.00", + "due_date": "2024-11-04", + "status": "waiting" + } + }, + { + "model": "calculator.instalment", + "pk": "6578316b-8c7a-4fe1-abe5-c640605b0c2f", + "fields": { + "created_at": "2024-11-04T11:20:46.842Z", + "modified_at": "2024-11-04T11:20:46.842Z", + "calculation": "aa52e56a-3d64-4129-ae7d-423585d47766", + "instalment_number": 2, + "amount": "1624.00", + "due_date": "2025-05-04", + "status": "waiting" + } + }, + + { + "model": "calculator.calculationrow", + "pk": "4d15a19e-828b-447a-9bc3-50519bac3d92", + "fields": { + "created_at": "2024-06-13T13:50:58.106Z", + "modified_at": "2024-06-13T13:50:58.106Z", + "calculation": "a698bcb1-abea-4da1-a447-cb3742017afe", + "row_type": "helsinki_benefit_monthly_eur", + "ordering": 36, + "description_fi": "Helsinki-lisä", + "amount": "250.00", + "start_date": null, + "end_date": null, + "description_type": null + } + }, + { + "model": "calculator.calculationrow", + "pk": "51965c5b-1cb3-4ffa-b4f4-133581752fa6", + "fields": { + "created_at": "2024-06-13T13:50:58.109Z", + "modified_at": "2024-06-13T13:50:58.109Z", + "calculation": "a698bcb1-abea-4da1-a447-cb3742017afe", + "row_type": "helsinki_benefit_sub_total_eur", + "ordering": 40, + "description_fi": "Yhteensä ajanjaksolta", + "amount": "1575.00", + "start_date": "2024-08-15", + "end_date": "2024-10-17", + "description_type": null + } + }, + { + "model": "calculator.calculationrow", + "pk": "3d2baa67-6bea-4ddb-a109-97c0eedd0b81", + "fields": { + "created_at": "2024-11-04T11:20:46.838Z", + "modified_at": "2024-11-04T11:20:46.838Z", + "calculation": "aa52e56a-3d64-4129-ae7d-423585d47766", + "row_type": "salary_costs", + "ordering": 0, + "description_fi": "Palkkauskustannukset", + "amount": "29997.00", + "start_date": null, + "end_date": null, + "description_type": null + } + }, + { + "model": "calculator.calculationrow", + "pk": "88660ef1-39eb-4323-b953-45dd60bf0a77", + "fields": { + "created_at": "2024-11-04T11:20:46.838Z", + "modified_at": "2024-11-04T11:20:46.838Z", + "calculation": "aa52e56a-3d64-4129-ae7d-423585d47766", + "row_type": "state_aid_max_monthly_eur", + "ordering": 1, + "description_fi": "Valtiotukimaksimi", + "amount": "29997.00", + "start_date": null, + "end_date": null, + "description_type": null + } + }, + { + "model": "calculator.calculationrow", + "pk": "54a20f0e-4a5e-4bde-8311-d1f24afaac8a", + "fields": { + "created_at": "2024-11-04T11:20:46.839Z", + "modified_at": "2024-11-04T11:20:46.839Z", + "calculation": "aa52e56a-3d64-4129-ae7d-423585d47766", + "row_type": "helsinki_benefit_monthly_eur", + "ordering": 2, + "description_fi": "Helsinki-lisä", + "amount": "800.00", + "start_date": null, + "end_date": null, + "description_type": null + } + }, + { + "model": "calculator.calculationrow", + "pk": "4afaa9d1-48f3-480b-936f-8999c1c9fb06", + "fields": { + "created_at": "2024-11-04T11:20:46.840Z", + "modified_at": "2024-11-04T11:20:46.840Z", + "calculation": "aa52e56a-3d64-4129-ae7d-423585d47766", + "row_type": "helsinki_benefit_sub_total_eur", + "ordering": 3, + "description_fi": "Yhteensä ajanjaksolta", + "amount": "11224.00", + "start_date": "2024-01-01", + "end_date": "2025-03-01", + "description_type": null + } + }, + { + "model": "calculator.calculationrow", + "pk": "52250d81-6f19-4304-9e15-3c4e63ada7a2", + "fields": { + "created_at": "2024-11-04T11:20:46.841Z", + "modified_at": "2024-11-04T11:20:46.841Z", + "calculation": "aa52e56a-3d64-4129-ae7d-423585d47766", + "row_type": "helsinki_benefit_total_eur", + "ordering": 4, + "description_fi": "Helsinki-lisä yhteensä", + "amount": "11224.00", + "start_date": "2024-01-01", + "end_date": "2025-03-01", + "description_type": "deduction" + } } ] diff --git a/backend/benefit/calculator/api/v1/serializers.py b/backend/benefit/calculator/api/v1/serializers.py index 91ff22903a..43d5a22790 100644 --- a/backend/benefit/calculator/api/v1/serializers.py +++ b/backend/benefit/calculator/api/v1/serializers.py @@ -6,9 +6,12 @@ from applications.enums import ApplicationActions, ApplicationStatus, BenefitType from applications.models import Application +from calculator.api.v1.validators import InstalmentStatusValidator +from calculator.enums import InstalmentStatus from calculator.models import ( Calculation, CalculationRow, + Instalment, PaySubsidy, PreviousBenefit, STATE_AID_MAX_PERCENTAGE_CHOICES, @@ -45,6 +48,36 @@ class Meta: ] +class InstalmentSerializer(serializers.ModelSerializer): + class Meta: + model = Instalment + fields = [ + "id", + "status", + "due_date", + "instalment_number", + "calculation", + "amount", + "created_at", + "modified_at", + ] + read_only_fields = [ + "id", + "due_date", + "instalment_number", + "calculation", + "amount", + "created_at", + "modified_at", + ] + + status = serializers.ChoiceField( + validators=[InstalmentStatusValidator()], + choices=InstalmentStatus.choices, + help_text="Status of the application, visible to the applicant", + ) + + class CalculationSerializer(serializers.ModelSerializer): id = serializers.UUIDField(required=False) rows = CalculationRowSerializer( diff --git a/backend/benefit/calculator/api/v1/validators.py b/backend/benefit/calculator/api/v1/validators.py new file mode 100644 index 0000000000..a718845ad7 --- /dev/null +++ b/backend/benefit/calculator/api/v1/validators.py @@ -0,0 +1,27 @@ +from applications.api.v1.status_transition_validator import StatusTransitionValidator +from calculator.enums import InstalmentStatus + + +class InstalmentStatusValidator(StatusTransitionValidator): + initial_status = InstalmentStatus.WAITING + + STATUS_TRANSITIONS = { + InstalmentStatus.WAITING: ( + InstalmentStatus.ACCEPTED, + InstalmentStatus.CANCELLED, + ), + InstalmentStatus.ACCEPTED: ( + InstalmentStatus.WAITING, + InstalmentStatus.PAID, + ), + InstalmentStatus.ERROR_IN_TALPA: ( + InstalmentStatus.WAITING, + InstalmentStatus.PAID, + ), + InstalmentStatus.PAID: (InstalmentStatus.COMPLETED,), + InstalmentStatus.CANCELLED: ( + InstalmentStatus.WAITING, + InstalmentStatus.COMPLETED, + ), + InstalmentStatus.COMPLETED: (), + } diff --git a/backend/benefit/calculator/api/v1/views.py b/backend/benefit/calculator/api/v1/views.py index a85b2d0105..40349805a9 100644 --- a/backend/benefit/calculator/api/v1/views.py +++ b/backend/benefit/calculator/api/v1/views.py @@ -1,9 +1,15 @@ +from django.shortcuts import get_object_or_404 from django_filters import rest_framework as filters from drf_spectacular.utils import extend_schema -from rest_framework import filters as drf_filters +from rest_framework import filters as drf_filters, status +from rest_framework.response import Response +from rest_framework.views import APIView -from calculator.api.v1.serializers import PreviousBenefitSerializer -from calculator.models import PreviousBenefit +from calculator.api.v1.serializers import ( + InstalmentSerializer, + PreviousBenefitSerializer, +) +from calculator.models import Instalment, PreviousBenefit from common.permissions import BFIsHandler from shared.audit_log.viewsets import AuditLoggingModelViewSet @@ -33,3 +39,16 @@ class PreviousBenefitViewSet(AuditLoggingModelViewSet): ] filterset_class = PreviousBenefitFilter search_fields = ["company__name", "social_security_number"] + + +class InstalmentView(APIView): + permission_classes = [BFIsHandler] + + def patch(self, request, instalment_id): + instalment = get_object_or_404(Instalment, pk=instalment_id) + + serializer = InstalmentSerializer(instalment, data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) diff --git a/backend/benefit/helsinkibenefit/urls.py b/backend/benefit/helsinkibenefit/urls.py index 4c3879efbe..6c6ccb2e6c 100644 --- a/backend/benefit/helsinkibenefit/urls.py +++ b/backend/benefit/helsinkibenefit/urls.py @@ -146,6 +146,10 @@ path( "v1/handlerapplications//review/", ReviewStateView.as_view() ), + path( + "v1/handlerinstalments//", + calculator_views.InstalmentView.as_view(), + ), path( "v1/handlerapplications//decisions/", DecisionTextList.as_view(), diff --git a/frontend/benefit/handler/public/locales/en/common.json b/frontend/benefit/handler/public/locales/en/common.json index cf1847a1bf..e3358aa033 100644 --- a/frontend/benefit/handler/public/locales/en/common.json +++ b/frontend/benefit/handler/public/locales/en/common.json @@ -187,8 +187,9 @@ "status": "Tila", "statusArchive": "Päätös", "ahjoStatus": "Päätös", - "talpaStatus": "Maksun tila", + "paymentStatus": "Maksun tila", "decisionDate": "Päätöspäivä", + "dueDate": "Eräpäivä", "benefitAmount": "Tuen määrä", "statuses": { "cancelled": "Peruutettu", @@ -219,6 +220,13 @@ "rejected_by_talpa": "Virhe maksussa", "successfully_sent_to_talpa": "Lähetetty maksuun" }, + "instalmentStatuses": { + "cancelled": "Peruttu", + "accepted": "Hyväksytty", + "waiting": "Odottaa", + "completed": "Valmis", + "error_in_talpa": "Virhe maksussa" + }, "calculationEndDate": "Viim. tukipäivä", "calculatedBenefitAmount": "Tukisumma" }, @@ -231,6 +239,7 @@ "archived": "Ei yhtään arkistoitua hakemusta.", "accepted,rejected": "Ei yhtään päätettävänä olevaa hakemusta.", "additional_information_needed": "Ei yhtään lisätietoja odottavaa hakemusta.", + "instalments": "Ei yhtään maksua odottavaa maksuerää.", "all": "Ei yhtään hakemusta.", "draft": "Ei yhtään luonnosta." } @@ -245,7 +254,8 @@ "infoRequired": "Odottaa lisätietoja", "decisions": "päätökset", "pending": "Päätettävänä", - "inPayment": "Maksussa" + "inPayment": "Maksussa", + "instalments": "Maksuerät" }, "errors": { "fetch": { @@ -267,7 +277,11 @@ "count_other": "Valittu {{count}} hakemusta" }, "actions": { - "addToBatch": "Lisää valitut koontiin" + "addToBatch": "Lisää valitut koontiin", + "return": "Palauta", + "confirm": "Hyväksy", + "cancel": "Peru", + "finish": "Siirrä arkistoon" } }, "pageHeaders": { @@ -1500,7 +1514,8 @@ "main": "Raportointi", "downloadAcceptedApplications": "Hyväksytyt hakemukset joita ei ole vielä ladattu", "downloadRejectedApplications": "Hylätyt hakemukset joita ei ole vielä ladattu", - "downloadApplicationsInTimeRange": "Lataa kaikki hakemukset tietyltä ajalta" }, + "downloadApplicationsInTimeRange": "Lataa kaikki hakemukset tietyltä ajalta" + }, "fields": { "lastDownloadDateText": "Ladattu viimeksi {{date}}", "startDate": "Alkaen", diff --git a/frontend/benefit/handler/public/locales/fi/common.json b/frontend/benefit/handler/public/locales/fi/common.json index 291716fb9e..a922c39852 100644 --- a/frontend/benefit/handler/public/locales/fi/common.json +++ b/frontend/benefit/handler/public/locales/fi/common.json @@ -187,8 +187,9 @@ "status": "Tila", "statusArchive": "Päätös tai tila", "ahjoStatus": "Päätös", - "talpaStatus": "Maksun tila", + "paymentStatus": "Maksun tila", "decisionDate": "Päätöspäivä", + "dueDate": "Eräpäivä", "benefitAmount": "Tuen määrä", "statuses": { "cancelled": "Peruutettu", @@ -215,9 +216,16 @@ "archival": "Myönteinen" }, "talpaStatuses": { - "not_sent_to_talpa": "Odottaa maksua", + "not_sent_to_talpa": "Odottaa", "rejected_by_talpa": "Virhe maksussa", - "successfully_sent_to_talpa": "Lähetetty maksuun" + "successfully_sent_to_talpa": "Maksettu" + }, + "instalmentStatuses": { + "cancelled": "Peruttu", + "accepted": "Hyväksytty", + "waiting": "Odottaa", + "completed": "Valmis", + "error_in_talpa": "Virhe maksussa" }, "calculationEndDate": "Viim. tukipäivä", "calculatedBenefitAmount": "Tukisumma" @@ -231,6 +239,7 @@ "archived": "Ei yhtään arkistoitua hakemusta.", "accepted,rejected": "Ei yhtään päätettävänä olevaa hakemusta.", "additional_information_needed": "Ei yhtään lisätietoja odottavaa hakemusta.", + "instalments": "Ei yhtään maksua odottavaa maksuerää.", "all": "Ei yhtään hakemusta.", "draft": "Ei yhtään luonnosta." } @@ -245,7 +254,8 @@ "infoRequired": "Odottaa lisätietoja", "decisions": "päätökset", "pending": "Päätettävänä", - "inPayment": "Maksussa" + "inPayment": "Maksussa", + "instalments": "Maksuerät" }, "errors": { "fetch": { @@ -267,7 +277,11 @@ "count_other": "Valittu {{count}} hakemusta" }, "actions": { - "addToBatch": "Lisää valitut koontiin" + "addToBatch": "Lisää valitut koontiin", + "return": "Palauta", + "confirm": "Hyväksy", + "cancel": "Peru", + "finish": "Siirrä arkistoon" } }, "pageHeaders": { diff --git a/frontend/benefit/handler/public/locales/sv/common.json b/frontend/benefit/handler/public/locales/sv/common.json index cf4f925f5a..e3358aa033 100644 --- a/frontend/benefit/handler/public/locales/sv/common.json +++ b/frontend/benefit/handler/public/locales/sv/common.json @@ -187,8 +187,9 @@ "status": "Tila", "statusArchive": "Päätös", "ahjoStatus": "Päätös", - "talpaStatus": "Maksun tila", + "paymentStatus": "Maksun tila", "decisionDate": "Päätöspäivä", + "dueDate": "Eräpäivä", "benefitAmount": "Tuen määrä", "statuses": { "cancelled": "Peruutettu", @@ -219,6 +220,13 @@ "rejected_by_talpa": "Virhe maksussa", "successfully_sent_to_talpa": "Lähetetty maksuun" }, + "instalmentStatuses": { + "cancelled": "Peruttu", + "accepted": "Hyväksytty", + "waiting": "Odottaa", + "completed": "Valmis", + "error_in_talpa": "Virhe maksussa" + }, "calculationEndDate": "Viim. tukipäivä", "calculatedBenefitAmount": "Tukisumma" }, @@ -231,6 +239,7 @@ "archived": "Ei yhtään arkistoitua hakemusta.", "accepted,rejected": "Ei yhtään päätettävänä olevaa hakemusta.", "additional_information_needed": "Ei yhtään lisätietoja odottavaa hakemusta.", + "instalments": "Ei yhtään maksua odottavaa maksuerää.", "all": "Ei yhtään hakemusta.", "draft": "Ei yhtään luonnosta." } @@ -245,7 +254,8 @@ "infoRequired": "Odottaa lisätietoja", "decisions": "päätökset", "pending": "Päätettävänä", - "inPayment": "Maksussa" + "inPayment": "Maksussa", + "instalments": "Maksuerät" }, "errors": { "fetch": { @@ -267,7 +277,11 @@ "count_other": "Valittu {{count}} hakemusta" }, "actions": { - "addToBatch": "Lisää valitut koontiin" + "addToBatch": "Lisää valitut koontiin", + "return": "Palauta", + "confirm": "Hyväksy", + "cancel": "Peru", + "finish": "Siirrä arkistoon" } }, "pageHeaders": { diff --git a/frontend/benefit/handler/src/components/applicationList/ApplicationList.sc.ts b/frontend/benefit/handler/src/components/applicationList/ApplicationList.sc.ts index 707e7626c2..a68faf401a 100644 --- a/frontend/benefit/handler/src/components/applicationList/ApplicationList.sc.ts +++ b/frontend/benefit/handler/src/components/applicationList/ApplicationList.sc.ts @@ -79,6 +79,14 @@ export const $AlterationBadge = styled.div<$AlterationBadgeProps>` export const $ApplicationList = styled.div``; +export const $InstalmentList = styled.div` + [class*='actionButtonContainer'] { + button { + display: none; + } + } +`; + type $ActionErrorsProps = { $errorText: string; }; diff --git a/frontend/benefit/handler/src/components/applicationList/ApplicationList.tsx b/frontend/benefit/handler/src/components/applicationList/ApplicationList.tsx index c9a66b7387..4448485fdf 100644 --- a/frontend/benefit/handler/src/components/applicationList/ApplicationList.tsx +++ b/frontend/benefit/handler/src/components/applicationList/ApplicationList.tsx @@ -18,6 +18,7 @@ import { sortFinnishDate, sortFinnishDateTime, } from 'shared/utils/date.utils'; +import { formatFloatToCurrency } from 'shared/utils/string.utils'; import { useTheme } from 'styled-components'; import { @@ -56,6 +57,25 @@ const buildApplicationUrl = ( return applicationUrl; }; +const getFirstInstalmentTotalAmount = ( + calculatedBenefitAmount: string, + pendingInstalmentAmount?: string +): string | JSX.Element => { + let firstInstalment = parseInt(calculatedBenefitAmount, 10); + if (pendingInstalmentAmount) { + firstInstalment -= parseInt(pendingInstalmentAmount, 10); + } + return pendingInstalmentAmount ? ( + <> + + {formatFloatToCurrency(firstInstalment, null, 'fi-FI', 0)} + {' '} + / {formatFloatToCurrency(calculatedBenefitAmount, 'EUR', 'fi-FI', 0)} + + ) : ( + formatFloatToCurrency(firstInstalment, 'EUR', 'fi-FI', 0) + ); +}; const dateForAdditionalInformationNeededBy = ( dateString: string | Date ): string => ` ${String(dateString).replace(/\d{4}$/, '')}`; @@ -141,6 +161,26 @@ const ApplicationList: React.FC = ({ [t, theme.colors.coatOfArms] ); + const renderTagWrapper = React.useCallback( + ( + applicationStatus: APPLICATION_STATUSES, + additionalInformationNeededBy: string | Date + ): JSX.Element => ( + <$TagWrapper $colors={getTagStyleForStatus(applicationStatus)}> + + {t( + `common:applications.list.columns.applicationStatuses.${String( + applicationStatus + )}` + )} + {applicationStatus === APPLICATION_STATUSES.INFO_REQUIRED && + dateForAdditionalInformationNeededBy(additionalInformationNeededBy)} + + + ), + [t] + ); + const columns = React.useMemo(() => { const cols: ApplicationListTableColumns[] = [ { @@ -239,21 +279,8 @@ const ApplicationList: React.FC = ({ transform: ({ status: applicationStatus, additionalInformationNeededBy, - }: ApplicationListTableTransforms) => ( - <$TagWrapper $colors={getTagStyleForStatus(applicationStatus)}> - - {t( - `common:applications.list.columns.applicationStatuses.${String( - applicationStatus - )}` - )} - {applicationStatus === APPLICATION_STATUSES.INFO_REQUIRED && - dateForAdditionalInformationNeededBy( - additionalInformationNeededBy - )} - - - ), + }: ApplicationListTableTransforms) => + renderTagWrapper(applicationStatus, additionalInformationNeededBy), headerName: getHeader('applicationStatus'), key: 'status', isSortable: true, @@ -306,8 +333,8 @@ const ApplicationList: React.FC = ({ isSortable: true, }, { - headerName: getHeader('talpaStatus'), - key: 'talpaStatus', + headerName: getHeader('paymentStatus'), + key: 'paymentStatus', isSortable: true, transform: ({ talpaStatus }) => t(`applications.list.columns.talpaStatuses.${String(talpaStatus)}`), @@ -317,7 +344,12 @@ const ApplicationList: React.FC = ({ key: 'calculatedBenefitAmount', transform: ({ calculatedBenefitAmount, - }: ApplicationListTableTransforms) => calculatedBenefitAmount, + pendingInstalment, + }: ApplicationListTableTransforms) => + getFirstInstalmentTotalAmount( + String(calculatedBenefitAmount), + String(pendingInstalment?.amount) || null + ), } ); } @@ -334,8 +366,9 @@ const ApplicationList: React.FC = ({ status, isAllStatuses, inPayment, - t, + renderTagWrapper, renderTableActions, + t, ]); if (isLoading) { diff --git a/frontend/benefit/handler/src/components/applicationList/ApplicationListForInstalments.tsx b/frontend/benefit/handler/src/components/applicationList/ApplicationListForInstalments.tsx new file mode 100644 index 0000000000..bdc7600b05 --- /dev/null +++ b/frontend/benefit/handler/src/components/applicationList/ApplicationListForInstalments.tsx @@ -0,0 +1,319 @@ +import { ROUTES } from 'benefit/handler/constants'; +import useInstalmentStatusTransition from 'benefit/handler/hooks/useInstalmentStatusTransition'; +import { + ApplicationListTableColumns, + ApplicationListTableTransforms, +} from 'benefit/handler/types/applicationList'; +import { getInstalmentTagStyleForStatus } from 'benefit/handler/utils/applications'; +import { + APPLICATION_STATUSES, + INSTALMENT_STATUSES, +} from 'benefit-shared/constants'; +import { ApplicationListItemData } from 'benefit-shared/types/application'; +import { + Button, + IconArrowUndo, + IconCheck, + IconCross, + IconDocument, + Table, + Tag, +} from 'hds-react'; +import * as React from 'react'; +import LoadingSkeleton from 'react-loading-skeleton'; +import { $Link } from 'shared/components/table/Table.sc'; +import { + convertToUIDateFormat, + sortFinnishDate, +} from 'shared/utils/date.utils'; +import { formatFloatToCurrency } from 'shared/utils/string.utils'; +import { useTheme } from 'styled-components'; + +import { + $Column, + $Wrapper, +} from '../applicationReview/actions/handlingApplicationActions/HandlingApplicationActions.sc'; +import { $HintText, $TableFooter } from '../table/TableExtras.sc'; +import { + $EmptyHeading, + $Heading, + $InstalmentList, + $TagWrapper, +} from './ApplicationList.sc'; +import { useApplicationList } from './useApplicationList'; + +export interface ApplicationListProps { + heading: string; + list?: ApplicationListItemData[]; + isLoading: boolean; +} + +const buildApplicationUrl = ( + id: string, + status: APPLICATION_STATUSES, + openDrawer = false +): string => { + if (status === APPLICATION_STATUSES.DRAFT) { + return `${ROUTES.APPLICATION_FORM_NEW}?id=${id}`; + } + + const applicationUrl = `${ROUTES.APPLICATION}?id=${id}`; + if (openDrawer) { + return `${applicationUrl}&openDrawer=1`; + } + return applicationUrl; +}; + +const ApplicationListForInstalments: React.FC = ({ + heading, + list = [], + isLoading = true, +}) => { + const { t, translationsBase, getHeader } = useApplicationList(); + const theme = useTheme(); + const [selectedRows, setSelectedRows] = React.useState([]); + const { mutate: changeInstalmentStatus, isLoading: isLoadingStatusChange } = + useInstalmentStatusTransition(); + + const columns = React.useMemo(() => { + const cols: ApplicationListTableColumns[] = [ + { + transform: ({ + id, + companyName, + unreadMessagesCount, + status: applicationStatus, + }: ApplicationListTableTransforms) => ( + <$Link + href={buildApplicationUrl( + id, + applicationStatus, + unreadMessagesCount > 0 + )} + > + {String(companyName)} + + ), + headerName: getHeader('companyName'), + key: 'companyName', + isSortable: true, + }, + + { + headerName: getHeader('companyId'), + key: 'companyId', + isSortable: true, + }, + { + headerName: getHeader('applicationNum'), + key: 'applicationNum', + isSortable: true, + }, + + // { + // headerName: getHeader('employeeName'), + // key: 'employeeName', + // isSortable: true, + // }, + ]; + + cols.push( + { + headerName: getHeader('dueDate'), + key: 'dueDate', + isSortable: true, + customSortCompareFunction: sortFinnishDate, + transform: ({ pendingInstalment }: ApplicationListTableTransforms) => + convertToUIDateFormat(pendingInstalment?.dueDate), + }, + + { + transform: ({ pendingInstalment }: ApplicationListTableTransforms) => ( + <$TagWrapper + $colors={getInstalmentTagStyleForStatus( + pendingInstalment?.status as INSTALMENT_STATUSES + )} + > + + {t( + `common:applications.list.columns.instalmentStatuses.${ + pendingInstalment?.status as INSTALMENT_STATUSES + }` + )} + + + ), + headerName: getHeader('paymentStatus'), + key: 'status', + isSortable: true, + }, + { + transform: ({ + calculatedBenefitAmount, + pendingInstalment, + }: ApplicationListTableTransforms) => ( + <> + + {formatFloatToCurrency( + pendingInstalment?.amount, + null, + 'fi-FI', + 0 + )}{' '} + + /{' '} + {formatFloatToCurrency(calculatedBenefitAmount, 'EUR', 'fi-FI', 0)} + + ), + headerName: getHeader('calculatedBenefitAmount'), + key: 'calculatedBenefitAmount', + isSortable: true, + } + ); + + return cols.filter(Boolean); + }, [getHeader, t]); + + if (isLoading) { + return ( + <> + {heading && ( + <$Heading + css={{ marginBottom: theme.spacing.xs }} + >{`${heading}`} + )} + + + + ); + } + + const selectedApplication = list.find((app) => app.id === selectedRows[0]); + const selectedInstalment = + list.find( + (app: ApplicationListItemData) => + app.id === String(selectedApplication?.id) + )?.pendingInstalment || null; + + return ( + <$InstalmentList data-testid="instalment-list"> + {list.length > 0 ? ( + <> + + <$TableFooter> + <$Wrapper> + <$Column> + {(selectedRows.length === 0 || selectedRows.length > 1) && ( + <$HintText>Valitse yksi hakemus + )} + + {selectedApplication && + selectedRows.length === 1 && + selectedInstalment && ( + <> + {selectedInstalment.status === + INSTALMENT_STATUSES.WAITING && ( + + )} + + {selectedInstalment.status === + INSTALMENT_STATUSES.WAITING && ( + + )} + + {[ + INSTALMENT_STATUSES.ACCEPTED, + INSTALMENT_STATUSES.CANCELLED, + ].includes( + selectedInstalment?.status as INSTALMENT_STATUSES + ) && ( + + )} + + {[ + INSTALMENT_STATUSES.CANCELLED, + INSTALMENT_STATUSES.PAID, + ].includes( + selectedInstalment?.status as INSTALMENT_STATUSES + ) && ( + + )} + + )} + + + + + ) : ( + <$EmptyHeading> + {t(`${translationsBase}.messages.empty.instalments`)} + + )} + + ); +}; + +export default ApplicationListForInstalments; diff --git a/frontend/benefit/handler/src/components/applicationList/HandlerIndex.tsx b/frontend/benefit/handler/src/components/applicationList/HandlerIndex.tsx index d3bdec4827..149688f059 100644 --- a/frontend/benefit/handler/src/components/applicationList/HandlerIndex.tsx +++ b/frontend/benefit/handler/src/components/applicationList/HandlerIndex.tsx @@ -15,6 +15,7 @@ import { useTheme } from 'styled-components'; import { $BackgroundWrapper } from '../layout/Layout'; import MainIngress from '../mainIngress/MainIngress'; import ApplicationList from './ApplicationList'; +import ApplicationListForInstalments from './ApplicationListForInstalments'; import { useApplicationList } from './useApplicationList'; export interface ApplicationListProps { @@ -48,7 +49,12 @@ const HandlerIndex: React.FC = ({ const theme = useTheme(); const getHeadingTranslation = ( - headingStatus: APPLICATION_STATUSES | 'all' | 'pending' | 'inPayment' + headingStatus: + | APPLICATION_STATUSES + | 'all' + | 'pending' + | 'inPayment' + | 'instalments' ): string => t(`${translationBase}.${headingStatus}`); const getTabCountPending = (): number => @@ -60,6 +66,9 @@ const HandlerIndex: React.FC = ({ !isBatchStatusHandlingComplete(app?.batch?.status) ).length; + const getTabCountInstalments = (): number => + list.filter((app: ApplicationListItemData) => app.pendingInstalment).length; + const getTabCountInPayment = (): number => list.filter( (app: ApplicationListItemData) => @@ -78,10 +87,12 @@ const HandlerIndex: React.FC = ({ const getTabCount = ( statuses: APPLICATION_STATUSES[], - handled?: 'inPayment' | 'pending' + handled?: 'inPayment' | 'pending' | 'instalments' ): number => { if (handled === 'pending') return getTabCountPending(); if (handled === 'inPayment') return getTabCountInPayment(); + if (handled === 'instalments') return getTabCountInstalments(); + if (handled === 'all') return getTabCountUndecided(); return list.filter((app: ApplicationListItemData) => statuses.includes(app.status) @@ -89,13 +100,18 @@ const HandlerIndex: React.FC = ({ }; const getListHeadingByStatus = ( - headingStatus: APPLICATION_STATUSES | 'all' | 'pending' | 'inPayment', + headingStatus: + | APPLICATION_STATUSES + | 'all' + | 'pending' + | 'inPayment' + | 'instalments', statuses: APPLICATION_STATUSES[] ): string => list && list?.length > 0 ? `${getHeadingTranslation(headingStatus)} (${getTabCount( statuses, - headingStatus as 'inPayment' | 'pending' + headingStatus as 'inPayment' | 'pending' | 'instalments' )})` : getHeadingTranslation(headingStatus); @@ -176,6 +192,15 @@ const HandlerIndex: React.FC = ({ APPLICATION_STATUSES.ACCEPTED, ])} + + updateTabToUrl(APPLICATION_LIST_TABS.PENDING_INSTALMENTS) + } + > + {getListHeadingByStatus('instalments', [ + APPLICATION_STATUSES.ACCEPTED, + ])} + @@ -258,6 +283,14 @@ const HandlerIndex: React.FC = ({ status={[APPLICATION_STATUSES.ACCEPTED]} /> + + + app.pendingInstalment)} + heading={t(`${translationBase}.instalments`)} + /> + diff --git a/frontend/benefit/handler/src/components/applicationList/HandlerIndexManual.tsx b/frontend/benefit/handler/src/components/applicationList/HandlerIndexManual.tsx new file mode 100644 index 0000000000..ac2e5c4f77 --- /dev/null +++ b/frontend/benefit/handler/src/components/applicationList/HandlerIndexManual.tsx @@ -0,0 +1,191 @@ +import { + ALL_APPLICATION_STATUSES, + APPLICATION_LIST_TABS, +} from 'benefit/handler/constants'; +import FrontPageProvider from 'benefit/handler/context/FrontPageProvider'; +import { APPLICATION_STATUSES } from 'benefit-shared/constants'; +import { ApplicationListItemData } from 'benefit-shared/types/application'; +import { LoadingSpinner, Tabs } from 'hds-react'; +import { useRouter } from 'next/router'; +import * as React from 'react'; +import Container from 'shared/components/container/Container'; +import { useTheme } from 'styled-components'; + +import ApplicationsHandled from '../batchProcessing/ApplicationsHandled'; +import { $BackgroundWrapper } from '../layout/Layout'; +import MainIngress from '../mainIngress/MainIngress'; +import ApplicationList from './ApplicationList'; +import { useApplicationList } from './useApplicationList'; + +export interface ApplicationListProps { + layoutBackgroundColor: string; + list?: ApplicationListItemData[]; + isLoading: boolean; +} +const translationBase = 'common:applications.list.headings'; + +const HandlerIndexManual: React.FC = ({ + layoutBackgroundColor, + list = [], + isLoading = true, +}) => { + const { t } = useApplicationList(); + const theme = useTheme(); + const router = useRouter(); + const { tab } = router.query; + + const [activeTab, setActiveTab] = React.useState(null); + + React.useEffect(() => { + if (!router.isReady) return; + setActiveTab(parseInt(tab as string, 10) || 0); + }, [router.isReady, tab]); + + if (activeTab === null) { + return ( +
+ +
+ ); + } + + const getHeadingTranslation = ( + headingStatus: APPLICATION_STATUSES | 'all' + ): string => t(`${translationBase}.${headingStatus}`); + + const getTabCount = (statuses: APPLICATION_STATUSES[]): number => + list.filter((app: ApplicationListItemData) => statuses.includes(app.status)) + .length; + + const getListHeadingByStatus = ( + headingStatus: APPLICATION_STATUSES | 'all', + statuses: APPLICATION_STATUSES[] + ): string => + list && list?.length > 0 + ? `${getHeadingTranslation(headingStatus)} (${getTabCount(statuses)})` + : getHeadingTranslation(headingStatus); + + const updateTabToUrl = (tabNumber: APPLICATION_LIST_TABS): void => + window.history.pushState({ tab }, '', `/?tab=${tabNumber}`); + + return ( + + <$BackgroundWrapper backgroundColor={layoutBackgroundColor}> + + + + + updateTabToUrl(APPLICATION_LIST_TABS.ALL)} + > + {getListHeadingByStatus('all', ALL_APPLICATION_STATUSES)} + + updateTabToUrl(APPLICATION_LIST_TABS.DRAFT)} + > + {getListHeadingByStatus(APPLICATION_STATUSES.DRAFT, [ + APPLICATION_STATUSES.DRAFT, + ])} + + updateTabToUrl(APPLICATION_LIST_TABS.RECEIVED)} + > + {getListHeadingByStatus(APPLICATION_STATUSES.RECEIVED, [ + APPLICATION_STATUSES.RECEIVED, + ])} + + updateTabToUrl(APPLICATION_LIST_TABS.HANDLING)} + > + {getListHeadingByStatus(APPLICATION_STATUSES.HANDLING, [ + APPLICATION_STATUSES.HANDLING, + APPLICATION_STATUSES.INFO_REQUIRED, + ])} + + updateTabToUrl(APPLICATION_LIST_TABS.ACCEPTED)} + > + {getListHeadingByStatus(APPLICATION_STATUSES.ACCEPTED, [ + APPLICATION_STATUSES.ACCEPTED, + ])} + + updateTabToUrl(APPLICATION_LIST_TABS.REJECTED)} + > + {getListHeadingByStatus(APPLICATION_STATUSES.REJECTED, [ + APPLICATION_STATUSES.REJECTED, + ])} + + + + + + + + + + [APPLICATION_STATUSES.DRAFT].includes(app.status) + )} + heading={t(`${translationBase}.draft`)} + status={[APPLICATION_STATUSES.DRAFT]} + /> + + + + + [APPLICATION_STATUSES.RECEIVED].includes(app.status) + )} + heading={t(`${translationBase}.received`)} + status={[APPLICATION_STATUSES.RECEIVED]} + /> + + + + + [APPLICATION_STATUSES.HANDLING].includes(app.status) + )} + heading={t(`${translationBase}.handling`)} + status={[APPLICATION_STATUSES.HANDLING]} + /> + + [APPLICATION_STATUSES.INFO_REQUIRED].includes(app.status) + )} + heading={t(`${translationBase}.infoRequired`)} + status={[APPLICATION_STATUSES.INFO_REQUIRED]} + /> + + + + + + + + + + + + + + ); +}; + +export default HandlerIndexManual; diff --git a/frontend/benefit/handler/src/components/applicationList/useApplicationListData.ts b/frontend/benefit/handler/src/components/applicationList/useApplicationListData.ts index 20f88e0db0..1796c0a696 100644 --- a/frontend/benefit/handler/src/components/applicationList/useApplicationListData.ts +++ b/frontend/benefit/handler/src/components/applicationList/useApplicationListData.ts @@ -11,7 +11,6 @@ import { convertToUIDateAndTimeFormat, convertToUIDateFormat, } from 'shared/utils/date.utils'; -import { formatFloatToCurrency } from 'shared/utils/string.utils'; interface ApplicationListProps { list: ApplicationListItemData[]; @@ -46,6 +45,7 @@ const useApplicationListData = ( handled_by_ahjo_automation, handled_at: handledAt, ahjo_error, + pending_instalment, } = application; return { @@ -75,12 +75,8 @@ const useApplicationListData = ( handledAt: convertToUIDateFormat(handledAt) || '-', ahjoError: camelcaseKeys(ahjo_error, { deep: true }) || null, decisionDate: convertToUIDateFormat(batch?.decision_date) || '-', - calculatedBenefitAmount: formatFloatToCurrency( - calculation?.calculated_benefit_amount || 0, - 'EUR', - 'fi-FI', - 0 - ), + calculatedBenefitAmount: calculation?.calculated_benefit_amount || '0', + pendingInstalment: camelcaseKeys(pending_instalment), }; }) .filter( diff --git a/frontend/benefit/handler/src/constants.ts b/frontend/benefit/handler/src/constants.ts index 3ca0277ca7..50466fa4c9 100644 --- a/frontend/benefit/handler/src/constants.ts +++ b/frontend/benefit/handler/src/constants.ts @@ -235,6 +235,7 @@ export enum APPLICATION_LIST_TABS { ACCEPTED = '4', REJECTED = '5', IN_PAYMENT = '5', + PENDING_INSTALMENTS = '6', } export const DEFAULT_MINIMUM_RECOVERY_AMOUNT = 20; diff --git a/frontend/benefit/handler/src/hooks/useInstalmentStatusTransition.ts b/frontend/benefit/handler/src/hooks/useInstalmentStatusTransition.ts new file mode 100644 index 0000000000..e2d0ca58d8 --- /dev/null +++ b/frontend/benefit/handler/src/hooks/useInstalmentStatusTransition.ts @@ -0,0 +1,48 @@ +import { AxiosError } from 'axios'; +import { HandlerEndpoint } from 'benefit-shared/backend-api/backend-api'; +import { INSTALMENT_STATUSES } from 'benefit-shared/constants'; +import { useTranslation } from 'next-i18next'; +import { useMutation, UseMutationResult, useQueryClient } from 'react-query'; +import showErrorToast from 'shared/components/toast/show-error-toast'; +import useBackendAPI from 'shared/hooks/useBackendAPI'; + +type Payload = { + id: string; + status: INSTALMENT_STATUSES; +}; + +const useInstalmentStatusTransition = (): UseMutationResult< + null, + Error, + Payload +> => { + const { axios, handleResponse } = useBackendAPI(); + const queryClient = useQueryClient(); + const { t } = useTranslation(); + + return useMutation( + 'instalments', + ({ id, status }: Payload) => + handleResponse( + axios.patch( + `${HandlerEndpoint.HANDLER_INSTALMENT_STATUS_TRANSITION(id)}`, + { status } + ) + ), + { + onSuccess: () => { + void queryClient.invalidateQueries('applicationsList'); + }, + onError: (error: AxiosError>) => { + showErrorToast( + t('common:error.generic.label'), + t('common:error.generic.text') + ); + // eslint-disable-next-line no-console + console.log(error); + }, + } + ); +}; + +export default useInstalmentStatusTransition; diff --git a/frontend/benefit/handler/src/pages/index.tsx b/frontend/benefit/handler/src/pages/index.tsx index d44d11696e..a0b1273c02 100644 --- a/frontend/benefit/handler/src/pages/index.tsx +++ b/frontend/benefit/handler/src/pages/index.tsx @@ -1,25 +1,15 @@ -import ApplicationList from 'benefit/handler/components/applicationList/ApplicationList'; -import ApplicationsHandled from 'benefit/handler/components/batchProcessing/ApplicationsHandled'; -import MainIngress from 'benefit/handler/components/mainIngress/MainIngress'; import AppContext from 'benefit/handler/context/AppContext'; -import FrontPageProvider from 'benefit/handler/context/FrontPageProvider'; import { useDetermineAhjoMode } from 'benefit/handler/hooks/useDetermineAhjoMode'; -import { APPLICATION_STATUSES } from 'benefit-shared/constants'; -import { ApplicationListItemData } from 'benefit-shared/types/application'; -import { LoadingSpinner, Tabs } from 'hds-react'; import { GetStaticProps, NextPage } from 'next'; -import { useRouter } from 'next/router'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; import * as React from 'react'; import { useEffect } from 'react'; -import Container from 'shared/components/container/Container'; import theme from 'shared/styles/theme'; import HandlerIndex from '../components/applicationList/HandlerIndex'; -import { useApplicationList } from '../components/applicationList/useApplicationList'; +import HandlerIndexManual from '../components/applicationList/HandlerIndexManual'; import { useApplicationListData } from '../components/applicationList/useApplicationListData'; -import { $BackgroundWrapper } from '../components/layout/Layout'; -import { ALL_APPLICATION_STATUSES, APPLICATION_LIST_TABS } from '../constants'; +import { ALL_APPLICATION_STATUSES } from '../constants'; const ApplicantIndex: NextPage = () => { const { @@ -40,50 +30,12 @@ const ApplicantIndex: NextPage = () => { }; }, [setIsFooterVisible, setIsNavigationVisible, setLayoutBackgroundColor]); - const translationBase = 'common:applications.list.headings'; - const isNewAhjoMode = useDetermineAhjoMode(); const { list, shouldShowSkeleton } = useApplicationListData( ALL_APPLICATION_STATUSES, !isNewAhjoMode ); - const { t } = useApplicationList(); - - const getHeadingTranslation = ( - headingStatus: APPLICATION_STATUSES | 'all' - ): string => t(`${translationBase}.${headingStatus}`); - - const getTabCount = (statuses: APPLICATION_STATUSES[]): number => - list.filter((app: ApplicationListItemData) => statuses.includes(app.status)) - .length; - - const getListHeadingByStatus = ( - headingStatus: APPLICATION_STATUSES | 'all', - statuses: APPLICATION_STATUSES[] - ): string => - list && list?.length > 0 - ? `${getHeadingTranslation(headingStatus)} (${getTabCount(statuses)})` - : getHeadingTranslation(headingStatus); - - const router = useRouter(); - const { tab } = router.query; - const [activeTab, setActiveTab] = React.useState(null); - const updateTabToUrl = (tabNumber: APPLICATION_LIST_TABS): void => - window.history.pushState({ tab }, '', `/?tab=${tabNumber}`); - - React.useEffect(() => { - if (!router.isReady) return; - setActiveTab(parseInt(tab as string, 10) || 0); - }, [router.isReady, tab]); - - if (activeTab === null) { - return ( -
- -
- ); - } return isNewAhjoMode ? ( { layoutBackgroundColor={layoutBackgroundColor} /> ) : ( - - <$BackgroundWrapper backgroundColor={layoutBackgroundColor}> - - - - - updateTabToUrl(APPLICATION_LIST_TABS.ALL)} - > - {getListHeadingByStatus('all', ALL_APPLICATION_STATUSES)} - - updateTabToUrl(APPLICATION_LIST_TABS.DRAFT)} - > - {getListHeadingByStatus(APPLICATION_STATUSES.DRAFT, [ - APPLICATION_STATUSES.DRAFT, - ])} - - updateTabToUrl(APPLICATION_LIST_TABS.RECEIVED)} - > - {getListHeadingByStatus(APPLICATION_STATUSES.RECEIVED, [ - APPLICATION_STATUSES.RECEIVED, - ])} - - updateTabToUrl(APPLICATION_LIST_TABS.HANDLING)} - > - {getListHeadingByStatus(APPLICATION_STATUSES.HANDLING, [ - APPLICATION_STATUSES.HANDLING, - APPLICATION_STATUSES.INFO_REQUIRED, - ])} - - updateTabToUrl(APPLICATION_LIST_TABS.ACCEPTED)} - > - {getListHeadingByStatus(APPLICATION_STATUSES.ACCEPTED, [ - APPLICATION_STATUSES.ACCEPTED, - ])} - - updateTabToUrl(APPLICATION_LIST_TABS.REJECTED)} - > - {getListHeadingByStatus(APPLICATION_STATUSES.REJECTED, [ - APPLICATION_STATUSES.REJECTED, - ])} - - - - - - - - - - [APPLICATION_STATUSES.DRAFT].includes(app.status) - )} - heading={t(`${translationBase}.draft`)} - status={[APPLICATION_STATUSES.DRAFT]} - /> - - - - - [APPLICATION_STATUSES.RECEIVED].includes(app.status) - )} - heading={t(`${translationBase}.received`)} - status={[APPLICATION_STATUSES.RECEIVED]} - /> - - - - - [APPLICATION_STATUSES.HANDLING].includes(app.status) - )} - heading={t(`${translationBase}.handling`)} - status={[APPLICATION_STATUSES.HANDLING]} - /> - - [APPLICATION_STATUSES.INFO_REQUIRED].includes(app.status) - )} - heading={t(`${translationBase}.infoRequired`)} - status={[APPLICATION_STATUSES.INFO_REQUIRED]} - /> - - - - - - - - - - - - - + ); }; diff --git a/frontend/benefit/handler/src/types/applicationList.d.ts b/frontend/benefit/handler/src/types/applicationList.d.ts index 2bc017550c..12d5e43e7b 100644 --- a/frontend/benefit/handler/src/types/applicationList.d.ts +++ b/frontend/benefit/handler/src/types/applicationList.d.ts @@ -1,5 +1,5 @@ import { APPLICATION_STATUSES } from 'benefit-shared/constants'; -import { AhjoError } from 'benefit-shared/types/application'; +import { AhjoError, Instalment } from 'benefit-shared/types/application'; export interface ApplicationListTableTransforms { id?: string; @@ -10,6 +10,7 @@ export interface ApplicationListTableTransforms { applicationOrigin?: APPLICATION_ORIGINS; ahjoError: AhjoError; calculatedBenefitAmount?: string; + pendingInstalment?: Instalment; } export interface ApplicationListTableColumns { diff --git a/frontend/benefit/handler/src/utils/applications.ts b/frontend/benefit/handler/src/utils/applications.ts index 33586778b8..49928e06b5 100644 --- a/frontend/benefit/handler/src/utils/applications.ts +++ b/frontend/benefit/handler/src/utils/applications.ts @@ -1,4 +1,7 @@ -import { APPLICATION_STATUSES } from 'benefit-shared/constants'; +import { + APPLICATION_STATUSES, + INSTALMENT_STATUSES, +} from 'benefit-shared/constants'; import theme from 'shared/styles/theme'; export const getTagStyleForStatus = ( @@ -49,3 +52,44 @@ export const getTagStyleForStatus = ( } return { background, text }; }; + +export const getInstalmentTagStyleForStatus = ( + status?: INSTALMENT_STATUSES +): { background: string; text: string } => { + let background: string; + let text: string = theme.colors.black; + switch (status) { + case INSTALMENT_STATUSES.WAITING: + background = theme.colors.black30; + text = theme.colors.white; + break; + + case INSTALMENT_STATUSES.ACCEPTED: + background = theme.colors.tramLight; + break; + + case INSTALMENT_STATUSES.CANCELLED: + background = theme.colors.summer; + break; + + case INSTALMENT_STATUSES.PAID: + background = theme.colors.tram; + text = theme.colors.white; + break; + + case INSTALMENT_STATUSES.ERROR_IN_TALPA: + background = theme.colors.error; + text = theme.colors.white; + break; + + case INSTALMENT_STATUSES.COMPLETED: + background = theme.colors.coatOfArms; + text = theme.colors.white; + break; + + default: + background = theme.colors.black40; + break; + } + return { background, text }; +}; diff --git a/frontend/benefit/shared/src/backend-api/backend-api.ts b/frontend/benefit/shared/src/backend-api/backend-api.ts index a82a3adec3..71b11e4e83 100644 --- a/frontend/benefit/shared/src/backend-api/backend-api.ts +++ b/frontend/benefit/shared/src/backend-api/backend-api.ts @@ -31,6 +31,7 @@ export const BackendEndpoint = { APPLICATIONS_CLONE_AS_DRAFT: 'v1/applications/clone_as_draft/', APPLICATIONS_CLONE_LATEST: 'v1/applications/clone_latest/', HANDLER_APPLICATIONS_CLONE_AS_DRAFT: 'v1/handlerapplications/clone_as_draft/', + HANDLER_INSTALMENTS: 'v1/handlerinstalments/', } as const; const batchBase = (id: string): string => @@ -39,6 +40,9 @@ const batchBase = (id: string): string => const handlerApplicationsBase = (id: string): string => `${BackendEndpoint.HANDLER_APPLICATIONS}${id}/`; +const handlerInstalmentBase = (id: string): string => + `${BackendEndpoint.HANDLER_INSTALMENTS}${id}/`; + export const HandlerEndpoint = { BATCH_APP_ASSIGN: `${BackendEndpoint.APPLICATION_BATCHES}assign_applications/`, BATCH_APP_DEASSIGN: (id: string): string => @@ -50,6 +54,8 @@ export const HandlerEndpoint = { `${BackendEndpoint.HANDLER_APPLICATIONS}batch_p2p_file?batch_id=${id}`, HANDLER_APPLICATIONS_CLONE_AS_DRAFT: (id: string) => `${handlerApplicationsBase(id)}clone_as_draft/`, + HANDLER_INSTALMENT_STATUS_TRANSITION: (id: string) => + `${handlerInstalmentBase(id)}`, } as const; const applicationsBase = (id: string): string => diff --git a/frontend/benefit/shared/src/constants.ts b/frontend/benefit/shared/src/constants.ts index 30336be8c3..4326fb6061 100644 --- a/frontend/benefit/shared/src/constants.ts +++ b/frontend/benefit/shared/src/constants.ts @@ -235,4 +235,13 @@ export enum PAY_SUBSIDY_PERCENT { export enum AHJO_STATUSES { DETAILS_RECEIVED = 'details_received', -} \ No newline at end of file +} + +export enum INSTALMENT_STATUSES { + WAITING = 'waiting', + ACCEPTED = 'accepted', + PAID = 'paid', + CANCELLED = 'cancelled', + ERROR_IN_TALPA = 'error_in_talpa', + COMPLETED = 'completed', +} diff --git a/frontend/benefit/shared/src/types/application.d.ts b/frontend/benefit/shared/src/types/application.d.ts index 04d030d35b..7227d6f701 100644 --- a/frontend/benefit/shared/src/types/application.d.ts +++ b/frontend/benefit/shared/src/types/application.d.ts @@ -419,6 +419,14 @@ export type PaySubsidyData = { duration_in_months_rounded: string; }; +export type Instalment = { + id: string; + instalmentNumber: number; + amount: string; + dueDate: string; + status: INSTALMENT_STATUSES; +}; + export type ApplicationData = { id?: string; status?: APPLICATION_STATUSES; @@ -491,6 +499,7 @@ export type ApplicationData = { handled_by_ahjo_automation?: boolean; alterations: ApplicationAlterationData[]; ahjo_error?: AhjoErrorData; + pending_instalment?: Instalment; }; export type EmployeeData = { @@ -591,6 +600,7 @@ export type ApplicationListItemData = { ahjoError?: AhjoError; decisionDate?: string; calculatedBenefitAmount?: string; + pendingInstalment?: Instalment; }; export type TextProp = 'textFi' | 'textEn' | 'textSv';