diff --git a/backend/benefit/README.md b/backend/benefit/README.md
index 5168f82fff..cb61d9cf2b 100644
--- a/backend/benefit/README.md
+++ b/backend/benefit/README.md
@@ -44,6 +44,7 @@ Load test fixtures
python manage.py loaddata default_terms.json
python manage.py loaddata groups.json
+ python manage.py loaddata test_applications.json
This creates terms of service and applicant terms in the database. The attachment PDF files are not actually
created by loading the fixture. In order to actually download the PDF files, log in via the django admin
diff --git a/backend/benefit/applications/api/v1/ahjo_decision_views.py b/backend/benefit/applications/api/v1/ahjo_decision_views.py
index a1fc8b3cde..af3e16e74f 100644
--- a/backend/benefit/applications/api/v1/ahjo_decision_views.py
+++ b/backend/benefit/applications/api/v1/ahjo_decision_views.py
@@ -2,17 +2,23 @@
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema, OpenApiParameter
from rest_framework import status
+from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.views import APIView
+from applications.api.v1.serializers.decision_proposal import (
+ AhjoDecisionProposalSerializer,
+)
from applications.api.v1.serializers.decision_proposal_template import (
DecisionProposalTemplateSectionSerializer,
)
from applications.api.v1.serializers.decision_text import DecisionTextSerializer
-from applications.enums import ApplicationStatus
+from applications.enums import ApplicationStatus, DecisionType
from applications.models import (
+ AhjoDecisionProposalDraft,
AhjoDecisionText,
Application,
+ ApplicationLogEntry,
DecisionProposalTemplateSection,
)
from applications.services.ahjo_decision_service import process_template_sections
@@ -42,10 +48,16 @@ class DecisionProposalTemplateSectionList(APIView):
)
def get(self, request, format=None) -> Response:
application_id = self.request.query_params.get("application_id")
+
try:
application = (
Application.objects.filter(
- pk=application_id, status=ApplicationStatus.ACCEPTED
+ pk=application_id,
+ status__in=[
+ ApplicationStatus.HANDLING,
+ ApplicationStatus.ACCEPTED,
+ ApplicationStatus.REJECTED,
+ ],
)
.prefetch_related("calculation", "company")
.first()
@@ -55,11 +67,15 @@ def get(self, request, format=None) -> Response:
{"message": "Application not found"}, status=status.HTTP_404_NOT_FOUND
)
+ language = (
+ application.applicant_language
+ if application.applicant_language == "sv"
+ else "fi"
+ )
decision_types = self.request.query_params.getlist("decision_type")
- section_types = self.request.query_params.getlist("section_type")
template_sections = DecisionProposalTemplateSection.objects.filter(
- section_type__in=section_types, decision_type__in=decision_types
+ decision_type__in=decision_types, language=language
)
replaced_template_sections = process_template_sections(
@@ -131,3 +147,76 @@ def put(self, request, application_id, decision_id):
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+
+
+class DecisionProposalDraftUpdate(APIView):
+ permission_classes = [BFIsHandler]
+
+ @action(methods=["PATCH"], detail=False)
+ def patch(self, request):
+ app_id = request.data["application_id"]
+ application = get_object_or_404(Application, id=app_id)
+
+ proposal_object = AhjoDecisionProposalDraft.objects.filter(
+ application=application
+ ).first()
+
+ proposal = AhjoDecisionProposalSerializer(
+ proposal_object, data=request.data, partial=True
+ )
+
+ if not proposal.is_valid():
+ return Response(proposal.errors, status=status.HTTP_400_BAD_REQUEST)
+
+ data = proposal.validated_data
+ proposal.save()
+
+ if data.get("decision_text") and data.get("justification_text"):
+ ahjo_text = AhjoDecisionText.objects.filter(application=application)
+ if ahjo_text:
+ ahjo_text.update(
+ language=application.applicant_language,
+ decision_type=(
+ DecisionType.ACCEPTED
+ if data["status"] == ApplicationStatus.ACCEPTED
+ else DecisionType.DENIED
+ ),
+ decision_text=data["decision_text"]
+ + "\n\n"
+ + data.get("justification_text"),
+ )
+ else:
+ AhjoDecisionText.objects.create(
+ application=application,
+ language=application.applicant_language,
+ decision_type=(
+ DecisionType.ACCEPTED
+ if data["status"] == ApplicationStatus.ACCEPTED
+ else DecisionType.DENIED
+ ),
+ decision_text=data.get("decision_text")
+ + "\n\n"
+ + data.get("justification_text"),
+ )
+
+ if data["review_step"] >= 4:
+ # Ensure frontend has a page that exists
+ proposal.review_step = 3
+ proposal.save()
+
+ ApplicationLogEntry.objects.create(
+ application=application,
+ from_status=application.status,
+ to_status=data["status"],
+ comment=data.get("log_entry_comment") or "",
+ )
+ application.status = data.get("status")
+
+ application.save()
+
+ application.calculation.granted_as_de_minimis_aid = data[
+ "granted_as_de_minimis_aid"
+ ]
+ application.calculation.save()
+
+ return Response(proposal.data, status=status.HTTP_200_OK)
diff --git a/backend/benefit/applications/api/v1/decision_proposal_draft_views.py b/backend/benefit/applications/api/v1/decision_proposal_draft_views.py
new file mode 100755
index 0000000000..1861831c19
--- /dev/null
+++ b/backend/benefit/applications/api/v1/decision_proposal_draft_views.py
@@ -0,0 +1,125 @@
+from django.db import transaction
+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, status, viewsets
+from rest_framework.decorators import action
+from rest_framework.response import Response
+
+from applications.api.v1.serializers.decision_proposal import (
+ AhjoDecisionProposalReadOnlySerializer,
+ AhjoDecisionProposalSerializer,
+)
+from applications.enums import ApplicationStatus, DecisionType
+from applications.models import AhjoDecisionProposalDraft, AhjoDecisionText, Application
+from common.permissions import BFIsHandler
+
+
+class AhjoDecisionProposalDraftFilter(filters.FilterSet):
+ class Meta:
+ model = AhjoDecisionProposalDraft
+ fields = {
+ "status": ["exact"],
+ }
+
+
+@extend_schema(
+ description=(
+ "API for create/read/update/delete/export operations on Helsinki benefit"
+ " application batches"
+ )
+)
+class AhjoDecisionProposalDraftViewSet(viewsets.ViewSet):
+ queryset = AhjoDecisionProposalDraft.objects.all()
+ serializer_class = AhjoDecisionProposalSerializer
+ permission_classes = [BFIsHandler]
+ filter_backends = [
+ drf_filters.OrderingFilter,
+ filters.DjangoFilterBackend,
+ drf_filters.SearchFilter,
+ ]
+ filterset_class = AhjoDecisionProposalDraftFilter
+
+ def get_queryset(self):
+ order_by = self.request.query_params.get("order_by") or None
+
+ if order_by:
+ self.queryset = self.queryset.order_by(order_by)
+
+ return self.queryset
+
+ def get_serializer_class(self):
+ """
+ ApplicationBatchSerializer for default behaviour on mutation functions,
+ ApplicationBatchListSerializer for listing applications on a batch
+ """
+ if self.action in ["create", "update", "partial_update", "destroy"]:
+ return AhjoDecisionProposalReadOnlySerializer
+
+ return AhjoDecisionProposalSerializer
+
+ @action(methods=["PATCH"], detail=False)
+ @transaction.atomic
+ def modify(self, request, pk=None):
+ """
+ Override default destroy(), batch can only be deleted if it's status is "draft"
+ """
+ app_id = request.data["application_id"]
+ application = get_object_or_404(Application, id=app_id)
+
+ proposal_object = AhjoDecisionProposalDraft.objects.filter(
+ application=application
+ ).first()
+
+ proposal = AhjoDecisionProposalSerializer(
+ proposal_object, data=request.data, partial=True
+ )
+
+ if not proposal.is_valid():
+ return Response(proposal.errors, status=status.HTTP_400_BAD_REQUEST)
+
+ data = proposal.validated_data
+ proposal.save()
+
+ if data.get("decision_text") and data.get("justification_text"):
+ ahjo_text = AhjoDecisionText.objects.filter(application=application)
+ if ahjo_text:
+ ahjo_text.update(
+ language=application.applicant_language,
+ decision_type=(
+ DecisionType.ACCEPTED
+ if data["status"] == ApplicationStatus.ACCEPTED
+ else DecisionType.DENIED
+ ),
+ decision_text=data["decision_text"]
+ + "\n\n"
+ + data["justification_text"],
+ )
+ else:
+ AhjoDecisionText.objects.create(
+ application=application,
+ language=application.applicant_language,
+ decision_type=(
+ DecisionType.ACCEPTED
+ if data["status"] == ApplicationStatus.ACCEPTED
+ else DecisionType.DENIED
+ ),
+ decision_text=data["decision_text"]
+ + "\n\n"
+ + data["justification_text"],
+ )
+
+ if data["review_step"] >= 4:
+ # Ensure GUI has a page that exists
+ proposal.review_step = 3
+ proposal.save()
+
+ application.status = data["status"]
+ application.save()
+
+ application.calculation.granted_as_de_minimis_aid = data[
+ "granted_as_de_minimis_aid"
+ ]
+ application.calculation.save()
+
+ return Response(proposal.data, status=status.HTTP_200_OK)
diff --git a/backend/benefit/applications/api/v1/serializers/application.py b/backend/benefit/applications/api/v1/serializers/application.py
index c2eed29de2..5d90bcbe0d 100755
--- a/backend/benefit/applications/api/v1/serializers/application.py
+++ b/backend/benefit/applications/api/v1/serializers/application.py
@@ -18,6 +18,9 @@
from applications.api.v1.serializers.attachment import AttachmentSerializer
from applications.api.v1.serializers.batch import ApplicationBatchSerializer
from applications.api.v1.serializers.de_minimis import DeMinimisAidSerializer
+from applications.api.v1.serializers.decision_proposal import (
+ AhjoDecisionProposalReadOnlySerializer,
+)
from applications.api.v1.serializers.employee import EmployeeSerializer
from applications.api.v1.serializers.utils import DynamicFieldsModelSerializer
from applications.api.v1.status_transition_validator import (
@@ -37,6 +40,7 @@
PaySubsidyGranted,
)
from applications.models import (
+ AhjoDecisionProposalDraft,
AhjoStatus,
Application,
ApplicationBasis,
@@ -1251,6 +1255,7 @@ def _base_update(self, instance, validated_data):
de_minimis_data = validated_data.pop("de_minimis_aid_set", None)
employee_data = validated_data.pop("employee", None)
approve_terms = validated_data.pop("approve_terms", None)
+ decision_proposal_data = validated_data.pop("decision_proposal", None)
pre_update_status = instance.status
application = super().update(instance, validated_data)
if de_minimis_data is not None:
@@ -1259,6 +1264,11 @@ def _base_update(self, instance, validated_data):
if employee_data is not None:
self._update_or_create_employee(application, employee_data)
+ if decision_proposal_data is not None:
+ AhjoDecisionProposalDraft.objects.update_or_create(
+ application=application, defaults=decision_proposal_data
+ )
+
if instance.status != pre_update_status:
self.handle_status_transition(
instance,
@@ -1284,6 +1294,9 @@ def create(self, validated_data):
self.assign_default_fields_from_company(application, validated_data["company"])
self._update_de_minimis_aid(application, de_minimis_data)
self._update_or_create_employee(application, employee_data)
+ AhjoDecisionProposalDraft.objects.update_or_create(
+ application=application, defaults={"review_step": 1}
+ )
return application
def _update_or_create_employee(self, application, employee_data):
@@ -1519,6 +1532,10 @@ class HandlerApplicationSerializer(BaseApplicationSerializer):
read_only=True, help_text="Application batch of this application, if any"
)
+ decision_proposal_draft = AhjoDecisionProposalReadOnlySerializer(
+ required=False, allow_null=True, read_only=True
+ )
+
create_application_for_company = serializers.UUIDField(
read_only=True,
required=False,
@@ -1576,6 +1593,7 @@ class Meta(BaseApplicationSerializer.Meta):
"handled_at",
"application_origin",
"paper_application_date",
+ "decision_proposal_draft",
]
read_only_fields = BaseApplicationSerializer.Meta.read_only_fields + [
"latest_decision_comment",
diff --git a/backend/benefit/applications/api/v1/serializers/decision_proposal.py b/backend/benefit/applications/api/v1/serializers/decision_proposal.py
new file mode 100644
index 0000000000..8e9ff09365
--- /dev/null
+++ b/backend/benefit/applications/api/v1/serializers/decision_proposal.py
@@ -0,0 +1,85 @@
+from django.core.exceptions import ValidationError
+from rest_framework import serializers
+
+from applications.enums import ApplicationStatus, HandlerRole
+from applications.models import AhjoDecisionProposalDraft
+
+
+class AhjoDecisionProposalReadOnlySerializer(serializers.ModelSerializer):
+ class Meta:
+ model = AhjoDecisionProposalDraft
+ fields = [
+ "granted_as_de_minimis_aid",
+ "status",
+ "decision_text",
+ "justification_text",
+ "review_step",
+ "handler_role",
+ "log_entry_comment",
+ ]
+
+
+class AhjoDecisionProposalSerializer(serializers.ModelSerializer):
+ """
+ # TODO: Add description
+
+ """
+
+ class Meta:
+ model = AhjoDecisionProposalDraft
+ fields = [
+ "granted_as_de_minimis_aid",
+ "status",
+ "decision_text",
+ "justification_text",
+ "review_step",
+ "handler_role",
+ "log_entry_comment",
+ ]
+
+ def validate(self, data):
+ errors = []
+ if data["review_step"] >= 2:
+ if data.get("status", None) not in [
+ ApplicationStatus.ACCEPTED,
+ ApplicationStatus.REJECTED,
+ ]:
+ errors.append(
+ ValidationError("Pending for status must ACCEPTED or REJECTED")
+ )
+ if (
+ data.get("status", None)
+ in [
+ ApplicationStatus.ACCEPTED,
+ ]
+ and data.get("granted_as_de_minimis_aid", None) is None
+ ):
+ errors.append(
+ ValidationError("Granted as de minimis aid must be true or false")
+ )
+ if (
+ data.get("status", None)
+ in [
+ ApplicationStatus.REJECTED,
+ ]
+ and len(data.get("log_entry_comment") or "") == 0
+ ):
+ errors.append(ValidationError("Log entry comment cannot be empty"))
+
+ if data["review_step"] >= 3:
+ if (
+ len(data.get("decision_text") or "") < 8
+ or len(data.get("justification_text") or "") < 8
+ ):
+ errors.append(
+ ValidationError("Decision or justification texts cannot be empty")
+ )
+ if (data.get("handler_role", None)) not in [
+ HandlerRole.HANDLER,
+ HandlerRole.MANAGER,
+ ]:
+ errors.append(ValidationError("Handler role must be specified"))
+ if len(errors) > 0:
+ raise ValidationError(errors)
+
+ return data
diff --git a/backend/benefit/applications/api/v1/serializers/decision_proposal_template.py b/backend/benefit/applications/api/v1/serializers/decision_proposal_template.py
index 43426a9ad0..b73c32d835 100644
--- a/backend/benefit/applications/api/v1/serializers/decision_proposal_template.py
+++ b/backend/benefit/applications/api/v1/serializers/decision_proposal_template.py
@@ -6,4 +6,9 @@
class DecisionProposalTemplateSectionSerializer(serializers.ModelSerializer):
class Meta:
model = DecisionProposalTemplateSection
- fields = ["id", "name", "section_type", "template_text"]
+ fields = [
+ "id",
+ "name",
+ "template_justification_text",
+ "template_decision_text",
+ ]
diff --git a/backend/benefit/applications/enums.py b/backend/benefit/applications/enums.py
index 0f4ab868b3..1ebd052a0e 100644
--- a/backend/benefit/applications/enums.py
+++ b/backend/benefit/applications/enums.py
@@ -195,15 +195,6 @@ class AhjoRequestType(models.TextChoices):
SEND_DECISION_PROPOSAL = "send_decision", _("Send decision to Ahjo")
-class DecisionProposalTemplateSectionType(models.TextChoices):
- DECISION_SECTION = "decision_section", _(
- "Template part for the decision section of a application decision proposal"
- )
- JUSTIFICATION_SECTION = "justification_section", _(
- "Template part for the decision justification section of a decision proposal"
- )
-
-
class DecisionType(models.TextChoices):
ACCEPTED = "accepted_decision", _("An accepted decision")
DENIED = "denied_decision", _("A denied decision")
@@ -220,6 +211,17 @@ class ApplicationAlterationState(models.TextChoices):
HANDLED = "handled", _("Handled")
+class HandlerRole(models.TextChoices):
+ HANDLER = "handler", _("Helsinki-benefit handler")
+ MANAGER = "manager", _("Team manager")
+
+
+class ApplicationReviewStep(models.TextChoices):
+ STEP_1 = "step_1", _("Step 1 - overview and accept/reject application")
+ STEP_2 = "step_2", _("Step 2 - preparing the decision")
+ STEP_3 = "step_3", _("Step 3 - review the decision")
+
+
# Call gettext on some of the enums so that "makemessages" command can find them when used dynamically in templates
_("granted")
_("granted_aged")
diff --git a/backend/benefit/applications/fixtures/test_applications.json b/backend/benefit/applications/fixtures/test_applications.json
new file mode 100644
index 0000000000..e4a397b31d
--- /dev/null
+++ b/backend/benefit/applications/fixtures/test_applications.json
@@ -0,0 +1,314 @@
+[
+ {
+ "model": "companies.company",
+ "pk": "746afc66-6f5a-4cb4-805f-4b58380b4745",
+ "fields": {
+ "name": "Automaattinen Testaus Keskus Oy",
+ "business_id": "5177536-7",
+ "company_form": "OY",
+ "street_address": "Reikäkorttitehtaankatu 3",
+ "postcode": "00120",
+ "city": "Helsinki",
+ "bank_account_number": "FI2112345600000785",
+ "company_form_code": 16
+ }
+ },
+ {
+ "model": "users.user",
+ "pk": "47ecedfa-351b-4815-bfac-96bdbc640178",
+ "fields": {
+ "password": "",
+ "last_login": "2024-03-26T13:46:05.974Z",
+ "is_superuser": false,
+ "username": "handler_examplesson",
+ "first_name": "Handy",
+ "last_name": "Examplesson",
+ "email": "handler-test@example.com",
+ "is_staff": true,
+ "is_active": true,
+ "date_joined": "2024-03-26T13:46:05.966Z",
+ "ad_username": null,
+ "groups": [],
+ "user_permissions": []
+ }
+ },
+ {
+ "model": "applications.application",
+ "pk": "fd89abe9-54e6-4112-a7ac-50147d95e530",
+ "fields": {
+ "created_at": "2024-03-26T13:45:24.607Z",
+ "modified_at": "2024-03-26T13:46:26.545Z",
+ "company": "746afc66-6f5a-4cb4-805f-4b58380b4745",
+ "status": "handling",
+ "talpa_status": "not_sent_to_talpa",
+ "application_origin": "applicant",
+ "application_number": 999999,
+ "company_name": "Automaattinen Testaus Keskus Oy",
+ "company_form": "OY",
+ "company_form_code": 16,
+ "company_department": "ATK Oy Tampereen keskusyksikkö",
+ "official_company_street_address": "Reikäkorttitehtaankatu 3",
+ "official_company_city": "Helsinki",
+ "official_company_postcode": "00120",
+ "use_alternative_address": true,
+ "alternative_company_street_address": "Toinentie 2",
+ "alternative_company_city": "Tampere",
+ "alternative_company_postcode": "33101",
+ "company_bank_account_number": "FI6033556370003404",
+ "company_contact_person_first_name": "Lumo",
+ "company_contact_person_last_name": "Tuomenkäpy",
+ "company_contact_person_phone_number": "+358501234567",
+ "company_contact_person_email": "lumo.tuanoinnii@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": "2024-02-01",
+ "paper_application_date": null,
+ "de_minimis_aid": false,
+ "batch": null,
+ "ahjo_case_id": null,
+ "ahjo_case_guid": null,
+ "bases": []
+ }
+ },
+ {
+ "model": "applications.attachment",
+ "pk": "70a660b2-f674-4238-8693-421b4af668e2",
+ "fields": {
+ "created_at": "2024-03-26T13:45:42.731Z",
+ "modified_at": "2024-03-26T13:45:42.731Z",
+ "application": "fd89abe9-54e6-4112-a7ac-50147d95e530",
+ "attachment_type": "employment_contract",
+ "content_type": "image/png",
+ "attachment_file": "1_BqII2rT.png",
+ "ahjo_version_series_id": null,
+ "ahjo_hash_value": null
+ }
+ },
+ {
+ "model": "applications.attachment",
+ "pk": "8744af7d-9624-4fc4-bbb0-31faaab6060d",
+ "fields": {
+ "created_at": "2024-03-26T13:45:46.601Z",
+ "modified_at": "2024-03-26T13:45:46.601Z",
+ "application": "fd89abe9-54e6-4112-a7ac-50147d95e530",
+ "attachment_type": "employee_consent",
+ "content_type": "image/png",
+ "attachment_file": "1_xELpIpP.png",
+ "ahjo_version_series_id": null,
+ "ahjo_hash_value": null
+ }
+ },
+ {
+ "model": "applications.employee",
+ "pk": "eff3b55d-b671-4ca7-be78-70cc0ee30279",
+ "fields": {
+ "created_at": "2024-03-26T13:45:24.616Z",
+ "modified_at": "2024-03-26T13:46:26.552Z",
+ "application": "fd89abe9-54e6-4112-a7ac-50147d95e530",
+ "encrypted_first_name": "Aari",
+ "encrypted_last_name": "Hömpömpö",
+ "first_name": "Aari",
+ "last_name": "Hömpömpö",
+ "encrypted_social_security_number": "051201U7113",
+ "social_security_number": "051201U7113",
+ "phone_number": "",
+ "email": "",
+ "employee_language": "fi",
+ "job_title": "Et atque enim possim",
+ "monthly_pay": "1595.00",
+ "vacation_money": "47.00",
+ "other_expenses": "18.00",
+ "working_hours": "168.00",
+ "collective_bargaining_agreement": "Dolor minus laboris",
+ "is_living_in_helsinki": true,
+ "commission_amount": null,
+ "commission_description": ""
+ }
+ },
+ {
+ "model": "applications.ahjodecisionproposaldraft",
+ "pk": 1,
+ "fields": {
+ "created_at": "2024-03-26T13:45:24.620Z",
+ "modified_at": "2024-03-26T13:45:24.620Z",
+ "application": "fd89abe9-54e6-4112-a7ac-50147d95e530",
+ "review_step": "1",
+ "status": null,
+ "log_entry_comment": null,
+ "granted_as_de_minimis_aid": false,
+ "handler_role": null,
+ "decision_text": null,
+ "justification_text": null
+ }
+ },
+ {
+ "model": "applications.reviewstate",
+ "pk": "fd89abe9-54e6-4112-a7ac-50147d95e530",
+ "fields": {
+ "paper": false,
+ "company": false,
+ "company_contact_person": false,
+ "de_minimis_aids": false,
+ "co_operation_negotiations": false,
+ "employee": false,
+ "pay_subsidy": false,
+ "benefit": false,
+ "employment": false,
+ "approval": false
+ }
+ },
+ {
+ "model": "applications.applicationlogentry",
+ "pk": "b0c8ee99-298e-4964-ab3f-8331462ba876",
+ "fields": {
+ "created_at": "2024-03-26T13:45:54.570Z",
+ "modified_at": "2024-03-26T13:45:54.570Z",
+ "application": "fd89abe9-54e6-4112-a7ac-50147d95e530",
+ "from_status": "draft",
+ "to_status": "received",
+ "comment": ""
+ }
+ },
+ {
+ "model": "applications.applicationlogentry",
+ "pk": "f776e9a1-bc93-46c9-871d-d484a5293247",
+ "fields": {
+ "created_at": "2024-03-26T13:46:09.717Z",
+ "modified_at": "2024-03-26T13:46:09.717Z",
+ "application": "fd89abe9-54e6-4112-a7ac-50147d95e530",
+ "from_status": "received",
+ "to_status": "handling",
+ "comment": ""
+ }
+ },
+ {
+ "model": "calculator.calculation",
+ "pk": "84e253ab-ebb2-434c-ab8a-48e52b0f1717",
+ "fields": {
+ "created_at": "2024-03-26T13:45:54.568Z",
+ "modified_at": "2024-03-26T13:46:26.569Z",
+ "handler": "47ecedfa-351b-4815-bfac-96bdbc640178",
+ "application": "fd89abe9-54e6-4112-a7ac-50147d95e530",
+ "monthly_pay": "1595.00",
+ "vacation_money": "47.00",
+ "other_expenses": "18.00",
+ "start_date": "2024-01-01",
+ "end_date": "2024-02-01",
+ "state_aid_max_percentage": 100,
+ "calculated_benefit_amount": "824.00",
+ "override_monthly_benefit_amount": null,
+ "granted_as_de_minimis_aid": false,
+ "target_group_check": false,
+ "override_monthly_benefit_amount_comment": ""
+ }
+ },
+ {
+ "model": "calculator.calculationrow",
+ "pk": "02893d7b-4a09-49bb-b1cf-530a96e6a343",
+ "fields": {
+ "created_at": "2024-03-26T13:46:26.568Z",
+ "modified_at": "2024-03-26T13:46:26.568Z",
+ "calculation": "84e253ab-ebb2-434c-ab8a-48e52b0f1717",
+ "row_type": "helsinki_benefit_total_eur",
+ "ordering": 4,
+ "description_fi": "Helsinki-lisä yhteensä",
+ "amount": "824.00",
+ "start_date": "2024-01-01",
+ "end_date": "2024-02-01",
+ "description_type": "deduction"
+ }
+ },
+ {
+ "model": "calculator.calculationrow",
+ "pk": "2612daa1-5772-4ee8-9081-72c592b1286c",
+ "fields": {
+ "created_at": "2024-03-26T13:46:26.565Z",
+ "modified_at": "2024-03-26T13:46:26.565Z",
+ "calculation": "84e253ab-ebb2-434c-ab8a-48e52b0f1717",
+ "row_type": "state_aid_max_monthly_eur",
+ "ordering": 1,
+ "description_fi": "Valtiotukimaksimi",
+ "amount": "1660.00",
+ "start_date": null,
+ "end_date": null,
+ "description_type": null
+ }
+ },
+ {
+ "model": "calculator.calculationrow",
+ "pk": "716ce541-1e0e-480d-b072-2b5359ef930d",
+ "fields": {
+ "created_at": "2024-03-26T13:46:26.566Z",
+ "modified_at": "2024-03-26T13:46:26.566Z",
+ "calculation": "84e253ab-ebb2-434c-ab8a-48e52b0f1717",
+ "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": "9702a546-ba01-45b7-8f65-241c692512b8",
+ "fields": {
+ "created_at": "2024-03-26T13:46:26.564Z",
+ "modified_at": "2024-03-26T13:46:26.564Z",
+ "calculation": "84e253ab-ebb2-434c-ab8a-48e52b0f1717",
+ "row_type": "salary_costs",
+ "ordering": 0,
+ "description_fi": "Palkkauskustannukset",
+ "amount": "1660.00",
+ "start_date": null,
+ "end_date": null,
+ "description_type": null
+ }
+ },
+ {
+ "model": "calculator.calculationrow",
+ "pk": "f9301e6f-e50a-468e-afc7-67a5d960fcc8",
+ "fields": {
+ "created_at": "2024-03-26T13:46:26.567Z",
+ "modified_at": "2024-03-26T13:46:26.567Z",
+ "calculation": "84e253ab-ebb2-434c-ab8a-48e52b0f1717",
+ "row_type": "helsinki_benefit_sub_total_eur",
+ "ordering": 3,
+ "description_fi": "Yhteensä ajanjaksolta",
+ "amount": "824.00",
+ "start_date": "2024-01-01",
+ "end_date": "2024-02-01",
+ "description_type": null
+ }
+ },
+ {
+ "model": "terms.applicanttermsapproval",
+ "pk": "06ebf56e-f9ac-4a84-9cc6-f5f55571e0c1",
+ "fields": {
+ "created_at": "2024-03-26T13:45:54.565Z",
+ "modified_at": "2024-03-26T13:45:54.565Z",
+ "approved_at": "2024-03-26T13:45:54.564Z",
+ "approved_by": "47ecedfa-351b-4815-bfac-96bdbc640178",
+ "terms": "1282f1dc-9019-4311-83d0-8f5956634392",
+ "application": "fd89abe9-54e6-4112-a7ac-50147d95e530",
+ "selected_applicant_consents": [
+ "1282f1dc-9019-4311-83d0-8f5956634392",
+ "061ed4a3-2195-46e0-a7d6-0859671bb277",
+ "d9a849c1-3700-4b04-9224-37d29c7abddf",
+ "f0478de2-bc68-469d-89ba-543ea040a0f6"
+ ]
+ }
+ }
+]
diff --git a/backend/benefit/applications/fixtures/test_decision_templates.json b/backend/benefit/applications/fixtures/test_decision_templates.json
new file mode 100644
index 0000000000..44a0c43ec2
--- /dev/null
+++ b/backend/benefit/applications/fixtures/test_decision_templates.json
@@ -0,0 +1,54 @@
+[
+ {
+ "model": "applications.decisionproposaltemplatesection",
+ "pk": "1b3b6c56-9195-42db-9a97-28f320b72937",
+ "fields": {
+ "created_at": "2024-03-27T08:00:24.562Z",
+ "modified_at": "2024-03-27T08:00:24.562Z",
+ "decision_type": "accepted_decision",
+ "language": "fi",
+ "template_decision_text": "
@role päätti myöntää $company:lle Työnantajan Helsinki-lisää käytettäväksi työllistetyn helsinkiläisen työllistämiseksi $total_amount euroa ajalle $benefit_date_range.
\nHelsinki-lisään on varattu talousarviossa Helsingin kaupungin Työllisyyspalveluille vuosittain budjetoitu määräraha. Avustuksen kustannukset maksetaan kaupungin Työllisyyspalveluille osoitetusta määrärahasta talousarvion erikseen määritellyltä kohdalta. Työnantajan Helsinki-lisä on aina harkinnanvarainen.
\n",
+ "template_justification_text": "Helsingin kaupunginhallituksen elinkeinojaosto on 11.9.2023 § 30 päättänyt tukea rahallisesti yksityisen ja kolmannen sektorin työnantajia, jotka tarjoavat työtä kaupungin työllisyydenhoidon kohderyhmiin kuuluville helsinkiläisille.
Avustus \nKaupunginhallituksen elinkeinojaosto on päätöksellään 11.9.2023 § 30 hyväksynyt työnantajan Helsinki-lisän myöntämistä koskevat ehdot. Helsinki-lisän myöntämisessä noudatetaan lisäksi kaupunginhallituksen 28.10.2019 § 723 hyväksymiä Helsingin kaupungin avustusten yleisohjeita.
Helsinki-lisässä on kyse työllistettävän henkilön palkkauskustannuksiin kohdistuvasta avustuksesta. Helsinki-lisä kohdistuu palkan sivukuluihin ja lomarahaan sekä bruttopalkan siihen osaan, jota palkkatuki ei kata. Tuen määrään vaikuttavat samoihin kustannuksiin myönnetyt muut tuet (esim. palkkatuki ja oppisopimuksen koulutuskorvaus). Yritykselle ja taloudellista toimintaa harjoittavalle yhteisölle avustus myönnetään vähämerkityksisenä tukena eli ns. de minimis -tukena, ei koskaan RPA-tukena. Yleishyödyllisille yhteisöille avustus myönnetään valtiontukisääntelyn ulkopuolisena tukena, jos yhdistys ei harjoita taloudellista toimintaa
\nAvustusta myönnetään valtiontukisäännöissä määrätyn kasautumissäännö sekä tuen enimmäisintensiteetin mukaisesti (tuen määrä suhteessa tukikelpoisiin kustannuksiin). Helsinki-lisä voi olla enintään 800 euroa kuukaudessa. Avustusta ei saa siirtää toisen tahon tai henkilön käytettäväksi. Avustus maksetaan hakemuksessa ilmoitetulle pankkitilille.
Helsinki-lisää saa käyttää ainoastaan kaupunginhallituksen elinkeinojaosto päätöksen 11.9.2023 §30 mukaisiin tarkoituksiin. Avustuksen saajan tulee viipymättä palauttaa virheellisesti, liikaa tai ilmeisen perusteettomasti saamansa avustus. Väärinkäyttötapauksessa kaupunki voi periä maksetun avustuksen takaisin.
\nAvustuksen saaja sitoutuu antamaan tarvittaessa kaupungille tarvittavat tiedot sen varmistamiseksi, että avustusta ei ole käytetty ehtojen vastaisesti. Mikäli avustus maksetaan ennen päätöksen lainvoimaisuutta, avustuksen saaja sitoutuu palauttamaan jo maksetut avustukset, jos päätös muutoksenhaun johdosta muuttuu.
Valtiontukiarviointi Yritykselle ja taloudellista toimintaa harjoittavalle yhteisölle avustus myönnetään vähämerkityksisenä tukena eli ns. de minimis -tukena. Tuen myöntämisessä noudatetaan komission asetusta (EU) 2023/2831, annettu 13.12.2023, Euroopan unionista tehdyn sopimuksen 107 ja 108 artiklan soveltamisesta vähämerkityksiseen tukeen (EUVL L2023/281, 15.12.2023).
Kullekin hakijalle myönnettävän de minimis -tuen määrä ilmenee hakijakohtaisesta liitteestä. Avustuksen saajalle voidaan myöntää de minimis -tukena enintään 300 000 euroa kuluvan vuoden ja kahden sitä edeltäneen kahden vuoden muodostaman jakson aikana. Avustuksen saaja vastaa siitä, että eri tahojen (mm. ministeriöt, ministeriöiden alaiset viranomaiset, Business Finland, Finnvera Oyj, kunnat, maakuntien liitot) myöntämien de minimis -tukien yhteismäärä ei ylitä tätä määrää. Avustuksen saaja on avustushakemuksessa ilmoittanut kaupungille kaikkien saamiensa de minimis -tukien määrät ja myöntöajankohdat.
\n",
+ "name": "FI: Myönteisen päätöksen Päätös-osion teksti"
+ }
+ },
+ {
+ "model": "applications.decisionproposaltemplatesection",
+ "pk": "24b4cbbc-04d9-4e61-aaab-6dd0d16d7654",
+ "fields": {
+ "created_at": "2024-03-27T07:58:20.550Z",
+ "modified_at": "2024-03-27T07:58:20.550Z",
+ "decision_type": "denied_decision",
+ "language": "fi",
+ "template_decision_text": "@role päätti hylätä $company:n hakemuksen koskien Työnantajan Helsinki-lisää, koska myöntämisen ehdot eivät täyty.
Helsinki-lisään on varattu talousarviossa Helsingin kaupungin Työllisyyspalveluille vuosittain budjetoitu määräraha. Avustuksen kustannukset maksetaan kaupungin Työllisyyspalveluille osoitetusta määrärahasta. Työnantajan Helsinki-lisä on aina harkinnanvarainen.
\n",
+ "template_justification_text": "Helsingin kaupunginhallituksen elinkeinojaosto on 11.9.2023 § 30 päättänyt tukea rahallisesti yksityisen ja kolmannen sektorin työnantajia, jotka tarjoavat työtä kaupungin työllisyydenhoidon kohderyhmiin kuuluville helsinkiläisille.
Lisätään/kirjoitetaan jokaisen päätöksen yksittäinen perustelu tähän.
Työnantajan hakemus ei täytä Helsinki-lisän ehtoja, joten avustusta ei voida myöntää.
Avustus \nKaupunginhallituksen elinkeinojaosto on päätöksellään 11.9.2023 § 30 hyväksynyt työnantajan Helsinki-lisän myöntämistä koskevat ehdot. Helsinki-lisän myöntämisessä noudatetaan lisäksi kaupunginhallituksen 28.10.2019 § 723 hyväksymiä Helsingin kaupungin avustusten yleisohjeita.
Helsinki-lisässä on kyse työllistettävän henkilön palkkauskustannuksiin kohdistuvasta avustuksesta. Helsinki-lisä kohdistuu palkan sivukuluihin ja lomarahaan sekä bruttopalkan siihen osaan, jota palkkatuki ei kata. Tuen määrään vaikuttavat samoihin kustannuksiin myönnetyt muut tuet (esim. palkkatuki ja oppisopimuksen koulutuskorvaus). Yritykselle ja taloudellista toimintaa harjoittavalle yhteisölle avustus myönnetään vähämerkityksisenä tukena eli ns. de minimis -tukena, ei koskaan RPA-tukena. Yleishyödyllisille yhteisöille avustus myönnetään valtiontukisääntelyn ulkopuolisena tukena, jos yhdistys ei harjoita taloudellista toimintaa
\nAvustusta myönnetään valtiontukisäännöissä määrätyn kasautumissäännö sekä tuen enimmäisintensiteetin mukaisesti (tuen määrä suhteessa tukikelpoisiin kustannuksiin). Helsinki-lisä voi olla enintään 800 euroa kuukaudessa. Avustusta ei saa siirtää toisen tahon tai henkilön käytettäväksi. Avustus maksetaan hakemuksessa ilmoitetulle pankkitilille.
Helsinki-lisää saa käyttää ainoastaan kaupunginhallituksen elinkeinojaosto päätöksen 11.9.2023 §30 mukaisiin tarkoituksiin. Avustuksen saajan tulee viipymättä palauttaa virheellisesti, liikaa tai ilmeisen perusteettomasti saamansa avustus. Väärinkäyttötapauksessa kaupunki voi periä maksetun avustuksen takaisin.
\nAvustuksen saaja sitoutuu antamaan tarvittaessa kaupungille tarvittavat tiedot sen varmistamiseksi, että avustusta ei ole käytetty ehtojen vastaisesti. Mikäli avustus maksetaan ennen päätöksen lainvoimaisuutta, avustuksen saaja sitoutuu palauttamaan jo maksetut avustukset, jos päätös muutoksenhaun johdosta muuttuu.
\n",
+ "name": "FI: Kielteisen päätöksen Päätöksen päätös-osion teksti"
+ }
+ },
+ {
+ "model": "applications.decisionproposaltemplatesection",
+ "pk": "2b60a08a-44a7-446a-8ed3-b842c0cb3e95",
+ "fields": {
+ "created_at": "2024-03-27T08:00:25.265Z",
+ "modified_at": "2024-03-27T08:00:25.265Z",
+ "decision_type": "accepted_decision",
+ "language": "sv",
+ "template_decision_text": "@role päätti myöntää $company:lle Työnantajan Helsinki-lisää käytettäväksi työllistetyn helsinkiläisen työllistämiseksi $total_amount euroa ajalle $benefit_date_range.
\nHelsinki-lisään on varattu talousarviossa Helsingin kaupungin Työllisyyspalveluille vuosittain budjetoitu määräraha. Avustuksen kustannukset maksetaan kaupungin Työllisyyspalveluille osoitetusta määrärahasta talousarvion erikseen määritellyltä kohdalta. Työnantajan Helsinki-lisä on aina harkinnanvarainen.
\n",
+ "template_justification_text": "Helsingin kaupunginhallituksen elinkeinojaosto on 11.9.2023 § 30 päättänyt tukea rahallisesti yksityisen ja kolmannen sektorin työnantajia, jotka tarjoavat työtä kaupungin työllisyydenhoidon kohderyhmiin kuuluville helsinkiläisille.
Avustus \nKaupunginhallituksen elinkeinojaosto on päätöksellään 11.9.2023 § 30 hyväksynyt työnantajan Helsinki-lisän myöntämistä koskevat ehdot. Helsinki-lisän myöntämisessä noudatetaan lisäksi kaupunginhallituksen 28.10.2019 § 723 hyväksymiä Helsingin kaupungin avustusten yleisohjeita.
Helsinki-lisässä on kyse työllistettävän henkilön palkkauskustannuksiin kohdistuvasta avustuksesta. Helsinki-lisä kohdistuu palkan sivukuluihin ja lomarahaan sekä bruttopalkan siihen osaan, jota palkkatuki ei kata. Tuen määrään vaikuttavat samoihin kustannuksiin myönnetyt muut tuet (esim. palkkatuki ja oppisopimuksen koulutuskorvaus). Yritykselle ja taloudellista toimintaa harjoittavalle yhteisölle avustus myönnetään vähämerkityksisenä tukena eli ns. de minimis -tukena, ei koskaan RPA-tukena. Yleishyödyllisille yhteisöille avustus myönnetään valtiontukisääntelyn ulkopuolisena tukena, jos yhdistys ei harjoita taloudellista toimintaa
\nAvustusta myönnetään valtiontukisäännöissä määrätyn kasautumissäännö sekä tuen enimmäisintensiteetin mukaisesti (tuen määrä suhteessa tukikelpoisiin kustannuksiin). Helsinki-lisä voi olla enintään 800 euroa kuukaudessa. Avustusta ei saa siirtää toisen tahon tai henkilön käytettäväksi. Avustus maksetaan hakemuksessa ilmoitetulle pankkitilille.
Helsinki-lisää saa käyttää ainoastaan kaupunginhallituksen elinkeinojaosto päätöksen 11.9.2023 §30 mukaisiin tarkoituksiin. Avustuksen saajan tulee viipymättä palauttaa virheellisesti, liikaa tai ilmeisen perusteettomasti saamansa avustus. Väärinkäyttötapauksessa kaupunki voi periä maksetun avustuksen takaisin.
\nAvustuksen saaja sitoutuu antamaan tarvittaessa kaupungille tarvittavat tiedot sen varmistamiseksi, että avustusta ei ole käytetty ehtojen vastaisesti. Mikäli avustus maksetaan ennen päätöksen lainvoimaisuutta, avustuksen saaja sitoutuu palauttamaan jo maksetut avustukset, jos päätös muutoksenhaun johdosta muuttuu.
Valtiontukiarviointi Yritykselle ja taloudellista toimintaa harjoittavalle yhteisölle avustus myönnetään vähämerkityksisenä tukena eli ns. de minimis -tukena. Tuen myöntämisessä noudatetaan komission asetusta (EU) 2023/2831, annettu 13.12.2023, Euroopan unionista tehdyn sopimuksen 107 ja 108 artiklan soveltamisesta vähämerkityksiseen tukeen (EUVL L2023/281, 15.12.2023).
Kullekin hakijalle myönnettävän de minimis -tuen määrä ilmenee hakijakohtaisesta liitteestä. Avustuksen saajalle voidaan myöntää de minimis -tukena enintään 300 000 euroa kuluvan vuoden ja kahden sitä edeltäneen kahden vuoden muodostaman jakson aikana. Avustuksen saaja vastaa siitä, että eri tahojen (mm. ministeriöt, ministeriöiden alaiset viranomaiset, Business Finland, Finnvera Oyj, kunnat, maakuntien liitot) myöntämien de minimis -tukien yhteismäärä ei ylitä tätä määrää. Avustuksen saaja on avustushakemuksessa ilmoittanut kaupungille kaikkien saamiensa de minimis -tukien määrät ja myöntöajankohdat.
\n",
+ "name": "SV: Myönteisen päätöksen Päätös-osion teksti"
+ }
+ },
+ {
+ "model": "applications.decisionproposaltemplatesection",
+ "pk": "35bebb28-3a13-4b1e-8013-ba547ac5cada",
+ "fields": {
+ "created_at": "2024-03-27T08:00:25.432Z",
+ "modified_at": "2024-03-27T08:00:25.432Z",
+ "decision_type": "denied_decision",
+ "language": "sv",
+ "template_decision_text": "@role päätti hylätä $company:n hakemuksen koskien Työnantajan Helsinki-lisää, koska myöntämisen ehdot eivät täyty.
Helsinki-lisään on varattu talousarviossa Helsingin kaupungin Työllisyyspalveluille vuosittain budjetoitu määräraha. Avustuksen kustannukset maksetaan kaupungin Työllisyyspalveluille osoitetusta määrärahasta. Työnantajan Helsinki-lisä on aina harkinnanvarainen.
\n",
+ "template_justification_text": "Helsingin kaupunginhallituksen elinkeinojaosto on 11.9.2023 § 30 päättänyt tukea rahallisesti yksityisen ja kolmannen sektorin työnantajia, jotka tarjoavat työtä kaupungin työllisyydenhoidon kohderyhmiin kuuluville helsinkiläisille.
Lisätään/kirjoitetaan jokaisen päätöksen yksittäinen perustelu tähän.
Työnantajan hakemus ei täytä Helsinki-lisän ehtoja, joten avustusta ei voida myöntää.
Avustus \nKaupunginhallituksen elinkeinojaosto on päätöksellään 11.9.2023 § 30 hyväksynyt työnantajan Helsinki-lisän myöntämistä koskevat ehdot. Helsinki-lisän myöntämisessä noudatetaan lisäksi kaupunginhallituksen 28.10.2019 § 723 hyväksymiä Helsingin kaupungin avustusten yleisohjeita.
Helsinki-lisässä on kyse työllistettävän henkilön palkkauskustannuksiin kohdistuvasta avustuksesta. Helsinki-lisä kohdistuu palkan sivukuluihin ja lomarahaan sekä bruttopalkan siihen osaan, jota palkkatuki ei kata. Tuen määrään vaikuttavat samoihin kustannuksiin myönnetyt muut tuet (esim. palkkatuki ja oppisopimuksen koulutuskorvaus). Yritykselle ja taloudellista toimintaa harjoittavalle yhteisölle avustus myönnetään vähämerkityksisenä tukena eli ns. de minimis -tukena, ei koskaan RPA-tukena. Yleishyödyllisille yhteisöille avustus myönnetään valtiontukisääntelyn ulkopuolisena tukena, jos yhdistys ei harjoita taloudellista toimintaa
\nAvustusta myönnetään valtiontukisäännöissä määrätyn kasautumissäännö sekä tuen enimmäisintensiteetin mukaisesti (tuen määrä suhteessa tukikelpoisiin kustannuksiin). Helsinki-lisä voi olla enintään 800 euroa kuukaudessa. Avustusta ei saa siirtää toisen tahon tai henkilön käytettäväksi. Avustus maksetaan hakemuksessa ilmoitetulle pankkitilille.
Helsinki-lisää saa käyttää ainoastaan kaupunginhallituksen elinkeinojaosto päätöksen 11.9.2023 §30 mukaisiin tarkoituksiin. Avustuksen saajan tulee viipymättä palauttaa virheellisesti, liikaa tai ilmeisen perusteettomasti saamansa avustus. Väärinkäyttötapauksessa kaupunki voi periä maksetun avustuksen takaisin.
\nAvustuksen saaja sitoutuu antamaan tarvittaessa kaupungille tarvittavat tiedot sen varmistamiseksi, että avustusta ei ole käytetty ehtojen vastaisesti. Mikäli avustus maksetaan ennen päätöksen lainvoimaisuutta, avustuksen saaja sitoutuu palauttamaan jo maksetut avustukset, jos päätös muutoksenhaun johdosta muuttuu.
\n",
+ "name": "SV: Kielteisen päätöksen Päätöksen päätös-osion teksti"
+ }
+ }
+]
diff --git a/backend/benefit/applications/management/commands/seed.py b/backend/benefit/applications/management/commands/seed.py
index 4a308470ca..dd6d8eaa6a 100755
--- a/backend/benefit/applications/management/commands/seed.py
+++ b/backend/benefit/applications/management/commands/seed.py
@@ -25,14 +25,12 @@
)
from applications.tests.factories import (
AcceptedDecisionProposalFactory,
- AcceptedDecisionProposalJustificationFactory,
AdditionalInformationNeededApplicationFactory,
ApplicationBatchFactory,
ApplicationWithAttachmentFactory,
CancelledApplicationFactory,
DecidedApplicationFactory,
DeniedDecisionProposalFactory,
- DeniedDecisionProposalJustificationFactory,
HandlingApplicationFactory,
ReceivedApplicationFactory,
RejectedApplicationFactory,
@@ -190,6 +188,4 @@ def _past_datetime(days: int) -> datetime:
def _create_templates():
AcceptedDecisionProposalFactory(),
- AcceptedDecisionProposalJustificationFactory(),
DeniedDecisionProposalFactory(),
- DeniedDecisionProposalJustificationFactory(),
diff --git a/backend/benefit/applications/migrations/0063_decision_proposal_template_changes.py b/backend/benefit/applications/migrations/0063_decision_proposal_template_changes.py
new file mode 100644
index 0000000000..281aaa885d
--- /dev/null
+++ b/backend/benefit/applications/migrations/0063_decision_proposal_template_changes.py
@@ -0,0 +1,48 @@
+# Generated by Django 3.2.23 on 2024-04-09 12:02
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("applications", "0062_alter_applicationalteration_recovery_amount"),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name="decisionproposaltemplatesection",
+ options={
+ "verbose_name": "Ahjo decision text template",
+ "verbose_name_plural": "Ahjo decision text templates",
+ },
+ ),
+ migrations.RemoveField(
+ model_name="decisionproposaltemplatesection",
+ name="section_type",
+ ),
+ migrations.RemoveField(
+ model_name="decisionproposaltemplatesection",
+ name="template_text",
+ ),
+ migrations.AddField(
+ model_name="decisionproposaltemplatesection",
+ name="template_decision_text",
+ field=models.TextField(null=True, verbose_name="Proposal text, decision"),
+ ),
+ migrations.AddField(
+ model_name="decisionproposaltemplatesection",
+ name="template_justification_text",
+ field=models.TextField(
+ null=True, verbose_name="Proposal text, justification"
+ ),
+ ),
+ migrations.AlterField(
+ model_name="decisionproposaltemplatesection",
+ name="name",
+ field=models.CharField(
+ max_length=256, verbose_name="name of the decision text template"
+ ),
+ ),
+ ]
diff --git a/backend/benefit/applications/migrations/0064_decision_proposal_drafts.py b/backend/benefit/applications/migrations/0064_decision_proposal_drafts.py
new file mode 100644
index 0000000000..68462de546
--- /dev/null
+++ b/backend/benefit/applications/migrations/0064_decision_proposal_drafts.py
@@ -0,0 +1,35 @@
+# Generated by Django 3.2.23 on 2024-04-09 12:02
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("applications", "0063_decision_proposal_template_changes"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='AhjoDecisionProposalDraft',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='time created')),
+ ('modified_at', models.DateTimeField(auto_now=True, verbose_name='time modified')),
+ ('review_step', models.CharField(blank=True, choices=[(1, 'step 1'), (2, 'step 2'), (3, 'step 3'), (4, 'submitted')], default=1, max_length=64, null=True, verbose_name='step of the draft proposal')),
+ ('status', models.CharField(blank=True, choices=[('accepted', 'accepted'), ('rejected', 'rejected')], max_length=64, null=True, verbose_name='Proposal status')),
+ ('log_entry_comment', models.TextField(blank=True, null=True, verbose_name='Log entry comment')),
+ ('granted_as_de_minimis_aid', models.BooleanField(default=False, null=True)),
+ ('handler_role', models.CharField(blank=True, choices=[('handler', 'Helsinki-benefit handler'), ('manager', 'Team manager')], max_length=64, null=True, verbose_name='Handler role')),
+ ('decision_text', models.TextField(blank=True, null=True, verbose_name='Decision text content')),
+ ('justification_text', models.TextField(blank=True, null=True, verbose_name='Justification text content')),
+ ('application', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='decision_proposal_draft', to='applications.application', verbose_name='application')),
+ ],
+ options={
+ 'verbose_name': 'decision_proposal_draft',
+ 'verbose_name_plural': 'decision_proposal_drafts',
+ 'db_table': 'bf_applications_decision_proposal_draft',
+ },
+ ),
+ ]
diff --git a/backend/benefit/applications/models.py b/backend/benefit/applications/models.py
index 50343ef59c..6f7e19eead 100755
--- a/backend/benefit/applications/models.py
+++ b/backend/benefit/applications/models.py
@@ -22,8 +22,8 @@
ApplicationTalpaStatus,
AttachmentType,
BenefitType,
- DecisionProposalTemplateSectionType,
DecisionType,
+ HandlerRole,
PaySubsidyGranted,
)
from applications.exceptions import (
@@ -980,13 +980,6 @@ class DecisionProposalTemplateSection(UUIDModel, TimeStampedModel):
text or the following justification text.
"""
- section_type = models.CharField(
- max_length=64,
- verbose_name=_("type of the decision proposal template section"),
- choices=DecisionProposalTemplateSectionType.choices,
- default=DecisionProposalTemplateSectionType.DECISION_SECTION,
- )
-
decision_type = models.CharField(
max_length=64,
verbose_name=_("type of the decision"),
@@ -1000,12 +993,16 @@ class DecisionProposalTemplateSection(UUIDModel, TimeStampedModel):
max_length=2,
)
- template_text = models.TextField(
- verbose_name=_("decision proposal section text content")
+ template_decision_text = models.TextField(
+ verbose_name=_("Proposal text, decision"), null=True
+ )
+
+ template_justification_text = models.TextField(
+ verbose_name=_("Proposal text, justification"), null=True
)
name = models.CharField(
- max_length=256, verbose_name=_("name of the decision proposal template section")
+ max_length=256, verbose_name=_("name of the decision text template")
)
def __str__(self):
@@ -1013,8 +1010,8 @@ def __str__(self):
class Meta:
db_table = "bf_applications_decision_proposal_template_section"
- verbose_name = _("decision proposal template section")
- verbose_name_plural = _("decision proposal template sections")
+ verbose_name = _("Ahjo decision text template")
+ verbose_name_plural = _("Ahjo decision text templates")
class AhjoDecisionText(UUIDModel, TimeStampedModel):
@@ -1141,3 +1138,72 @@ class ApplicationAlteration(TimeStampedModel):
contact_person_name = models.TextField(
verbose_name=_("contact person"),
)
+
+
+class AhjoDecisionProposalDraft(TimeStampedModel):
+ """
+ Draft of decision proposal. Supports application handling
+ when drafting a decision that ultimately leads to Ahjo handling and decision.
+ """
+
+ class Meta:
+ db_table = "bf_applications_decision_proposal_draft"
+ verbose_name = _("decision_proposal_draft")
+ verbose_name_plural = _("decision_proposal_drafts")
+
+ application = models.OneToOneField(
+ Application,
+ verbose_name=_("application"),
+ related_name="decision_proposal_draft",
+ on_delete=models.CASCADE,
+ )
+
+ review_step = models.CharField(
+ max_length=64,
+ verbose_name=_("step of the draft proposal"),
+ blank=True,
+ null=True,
+ default=1,
+ choices=[(1, "step 1"), (2, "step 2"), (3, "step 3"), (4, "submitted")],
+ )
+
+ status = models.CharField(
+ max_length=64,
+ verbose_name=_("Proposal status"),
+ choices=[
+ (ApplicationStatus.ACCEPTED, "accepted"),
+ (ApplicationStatus.REJECTED, "rejected"),
+ ],
+ null=True,
+ blank=True,
+ )
+
+ log_entry_comment = models.TextField(
+ verbose_name=_("Log entry comment"),
+ blank=True,
+ null=True,
+ )
+
+ granted_as_de_minimis_aid = models.BooleanField(
+ default=False, null=True, blank=False
+ )
+
+ handler_role = models.CharField(
+ max_length=64,
+ verbose_name=_("Handler role"),
+ blank=True,
+ null=True,
+ choices=HandlerRole.choices,
+ )
+
+ decision_text = models.TextField(
+ verbose_name=_("Decision text content"),
+ blank=True,
+ null=True,
+ )
+
+ justification_text = models.TextField(
+ verbose_name=_("Justification text content"),
+ blank=True,
+ null=True,
+ )
diff --git a/backend/benefit/applications/services/ahjo_decision_service.py b/backend/benefit/applications/services/ahjo_decision_service.py
index 5f55856f34..cc99ce1145 100644
--- a/backend/benefit/applications/services/ahjo_decision_service.py
+++ b/backend/benefit/applications/services/ahjo_decision_service.py
@@ -3,7 +3,7 @@
from django.conf import settings
-from applications.enums import DecisionProposalTemplateSectionType, DecisionType
+from applications.enums import DecisionType, HandlerRole
from applications.models import (
AhjoDecisionText,
Application,
@@ -11,9 +11,7 @@
)
from applications.tests.factories import (
AcceptedDecisionProposalFactory,
- AcceptedDecisionProposalJustificationFactory,
DeniedDecisionProposalFactory,
- DeniedDecisionProposalJustificationFactory,
)
@@ -23,19 +21,32 @@ class AhjoDecisionError(Exception):
def replace_decision_template_placeholders(
text_to_replace: str,
+ decision_type: DecisionType,
application: Application,
- decision_maker: str = "Päättäjä x (Helsinki-lisä-suunnittelija/Tiimipäällikkö)",
+ decision_maker: HandlerRole = HandlerRole.HANDLER,
) -> str:
"""Replace the placeholders starting with $ in the decision template with real data"""
text_to_replace = Template(text_to_replace)
- start_date = application.calculation.start_date.strftime("%d.%m.%Y")
- end_date = application.calculation.end_date.strftime("%d.%m.%Y")
+ start_date = (
+ application.calculation.start_date.strftime("%d.%m.%Y")
+ if decision_type == DecisionType.ACCEPTED
+ else application.start_date
+ )
+ end_date = (
+ application.calculation.end_date.strftime("%d.%m.%Y")
+ if decision_type == DecisionType.ACCEPTED
+ else application.end_date
+ )
try:
return text_to_replace.substitute(
- decision_maker=decision_maker,
company=application.company.name,
- total_amount=application.calculation.calculated_benefit_amount,
- benefit_date_range=f"{start_date} - {end_date}",
+ decision_maker=decision_maker,
+ total_amount=(
+ application.calculation.calculated_benefit_amount
+ if decision_type == DecisionType.ACCEPTED
+ else ""
+ ),
+ benefit_date_range=(f"{start_date} - {end_date}"),
)
except AhjoDecisionError as e:
raise ValueError(f"Error in preparing the decision proposal template: {e}")
@@ -48,10 +59,9 @@ def process_template_sections(
"""Loop through the template sections and conditionally
replace placeholders if section is a decision section"""
for section in template_sections:
- if section.section_type == DecisionProposalTemplateSectionType.DECISION_SECTION:
- section.template_text = replace_decision_template_placeholders(
- section.template_text, application
- )
+ section.template_decision_text = replace_decision_template_placeholders(
+ section.template_decision_text, section.decision_type, application
+ )
return template_sections
@@ -84,11 +94,11 @@ def _generate_decision_text_string(
) -> str:
if decision_type == DecisionType.ACCEPTED:
decision_section = AcceptedDecisionProposalFactory()
- justification_section = AcceptedDecisionProposalJustificationFactory()
else:
decision_section = DeniedDecisionProposalFactory()
- justification_section = DeniedDecisionProposalJustificationFactory()
- decision_string = f"""Päätös {decision_section.template_text} \
-Päätösteksti {justification_section.template_text} """
-
- return replace_decision_template_placeholders(decision_string, application)
+ decision_string = f"""Päätös {decision_section.template_decision_text} \
+Päätösteksti {decision_section.template_justification_text} """ # noqa
+ decision_type = decision_section.decision_type
+ return replace_decision_template_placeholders(
+ decision_string, decision_type, application
+ )
diff --git a/backend/benefit/applications/services/clamav.py b/backend/benefit/applications/services/clamav.py
index 754f9895ec..428ba900c7 100644
--- a/backend/benefit/applications/services/clamav.py
+++ b/backend/benefit/applications/services/clamav.py
@@ -11,7 +11,7 @@ class ClamavClient:
BASE_URL = None
def __init__(self):
- if not settings.CLAMAV_URL:
+ if not settings.CLAMAV_URL and not settings.NEXT_PUBLIC_MOCK_FLAG:
log.warning(
"CLAMAV_URL not defined in settings -- Clamav Client not enabled"
)
diff --git a/backend/benefit/applications/tests/conftest.py b/backend/benefit/applications/tests/conftest.py
index 7c019ea0e5..7062ff3567 100755
--- a/backend/benefit/applications/tests/conftest.py
+++ b/backend/benefit/applications/tests/conftest.py
@@ -418,7 +418,9 @@ def denied_ahjo_decision_section():
def accepted_ahjo_decision_text(decided_application):
template = AcceptedDecisionProposalFactory()
replaced_decision_text = replace_decision_template_placeholders(
- template.template_text, decided_application
+ template.template_decision_text + template.template_justification_text,
+ DecisionType.ACCEPTED,
+ decided_application,
)
return AhjoDecisionTextFactory(
decision_type=DecisionType.ACCEPTED,
diff --git a/backend/benefit/applications/tests/factories.py b/backend/benefit/applications/tests/factories.py
index 0d539bb0c1..541ff31d5d 100755
--- a/backend/benefit/applications/tests/factories.py
+++ b/backend/benefit/applications/tests/factories.py
@@ -9,12 +9,12 @@
ApplicationStatus,
ApplicationStep,
BenefitType,
- DecisionProposalTemplateSectionType,
DecisionType,
PaySubsidyGranted,
)
from applications.models import (
AhjoDecision,
+ AhjoDecisionProposalDraft,
AhjoDecisionText,
Application,
APPLICATION_LANGUAGE_CHOICES,
@@ -70,6 +70,11 @@ class Meta:
model = DeMinimisAid
+class AhjoDecisionProposalDraftFactory(factory.django.DjangoModelFactory):
+ class Meta:
+ model = AhjoDecisionProposalDraft
+
+
class ApplicationBasisFactory(factory.django.DjangoModelFactory):
identifier = factory.Sequence(
lambda id: f"basis_identifier_{id}"
@@ -146,6 +151,11 @@ def bases(self, created, extracted, **kwargs):
factory_related_name="application",
)
+ decision_proposal_draft = factory.RelatedFactory(
+ AhjoDecisionProposalDraftFactory,
+ factory_related_name="application",
+ )
+
class Meta:
model = Application
@@ -370,10 +380,9 @@ class Meta:
class AcceptedDecisionProposalFactory(factory.django.DjangoModelFactory):
- section_type = DecisionProposalTemplateSectionType.DECISION_SECTION
decision_type = DecisionType.ACCEPTED
name = "Myönteisen päätöksen Päätös-osion teksti"
- template_text = """$decision_maker päätti myöntää $company:lle Työnantajan Helsinki-lisää \
+ template_decision_text = """
$decision_maker päätti myöntää $company:lle Työnantajan Helsinki-lisää \
käytettäväksi työllistetyn helsinkiläisen työllistämiseksi $total_amount euroa ajalle $benefit_date_range.
Helsinki-lisään on varattu talousarviossa Helsingin kaupungin Työllisyyspalveluille \
vuosittain budjetoitu määräraha. Avustuksen kustannukset maksetaan \
@@ -381,15 +390,7 @@ class AcceptedDecisionProposalFactory(factory.django.DjangoModelFactory):
erikseen määritellyltä kohdalta. Työnantajan Helsinki-lisä on aina harkinnanvarainen.
"""
- class Meta:
- model = DecisionProposalTemplateSection
-
-
-class AcceptedDecisionProposalJustificationFactory(factory.django.DjangoModelFactory):
- section_type = DecisionProposalTemplateSectionType.JUSTIFICATION_SECTION
- decision_type = DecisionType.ACCEPTED
- name = "Myönteisen Päätöksen perustelut-osion teksti"
- template_text = """Helsingin kaupunginhallituksen elinkeinojaosto on 11.9.2023 § 30 päättänyt \
+ template_justification_text = """
Helsingin kaupunginhallituksen elinkeinojaosto on 11.9.2023 § 30 päättänyt \
tukea rahallisesti yksityisen ja kolmannen sektorin työnantajia, \
jotka tarjoavat työtä kaupungin työllisyydenhoidon \
kohderyhmiin kuuluville helsinkiläisille.
\
@@ -442,25 +443,15 @@ class Meta:
class DeniedDecisionProposalFactory(factory.django.DjangoModelFactory):
- section_type = DecisionProposalTemplateSectionType.DECISION_SECTION
decision_type = DecisionType.DENIED
name = "Kielteisen päätöksen Päätöksen päätös-osion teksti"
- template_text = """$decision_maker päätti hylätä $company:n hakemuksen koskien Työnantajan \
+ template_decision_text = """
$decision_maker päätti hylätä $company:n hakemuksen koskien Työnantajan \
Helsinki-lisää, koska myöntämisen ehdot eivät täyty.
\
Helsinki-lisään on varattu talousarviossa Helsingin kaupungin Työllisyyspalveluille \
vuosittain budjetoitu määräraha. Avustuksen kustannukset maksetaan kaupungin \
Työllisyyspalveluille osoitetusta määrärahasta. Työnantajan Helsinki-lisä on aina harkinnanvarainen.
"""
-
- class Meta:
- model = DecisionProposalTemplateSection
-
-
-class DeniedDecisionProposalJustificationFactory(factory.django.DjangoModelFactory):
- section_type = DecisionProposalTemplateSectionType.JUSTIFICATION_SECTION
- decision_type = DecisionType.DENIED
- name = "Kielteisen päätöksen Päätöksen perustelut-osion teksti"
- template_text = """Helsingin kaupunginhallituksen elinkeinojaosto on 11.9.2023 § 30 päättänyt \
+ template_justification_text = """
Helsingin kaupunginhallituksen elinkeinojaosto on 11.9.2023 § 30 päättänyt \
tukea rahallisesti yksityisen ja kolmannen sektorin työnantajia, \
jotka tarjoavat työtä kaupungin työllisyydenhoidon kohderyhmiin \
kuuluville helsinkiläisille.
\
diff --git a/backend/benefit/applications/tests/test_ahjo_decision_proposal_drafts.py b/backend/benefit/applications/tests/test_ahjo_decision_proposal_drafts.py
new file mode 100644
index 0000000000..2db5e786fe
--- /dev/null
+++ b/backend/benefit/applications/tests/test_ahjo_decision_proposal_drafts.py
@@ -0,0 +1,143 @@
+from datetime import date
+
+import pytest
+from rest_framework.reverse import reverse
+
+from applications.enums import ApplicationStatus
+from applications.models import AhjoDecisionText
+from calculator.models import Calculation
+from calculator.tests.factories import PaySubsidyFactory
+
+
+def _prepare_calculation(application):
+ application.calculation = Calculation(
+ application=application,
+ monthly_pay=1234,
+ vacation_money=123,
+ other_expenses=321,
+ start_date=application.start_date,
+ end_date=application.end_date,
+ state_aid_max_percentage=50,
+ calculated_benefit_amount=0,
+ override_monthly_benefit_amount=None,
+ )
+ pay_subsidy = PaySubsidyFactory(
+ pay_subsidy_percent=50, start_date=date(2021, 7, 10), end_date=date(2021, 9, 10)
+ )
+ application.pay_subsidies.add(pay_subsidy)
+ application.calculation.save()
+ application.save()
+
+
+@pytest.mark.parametrize(
+ """response_status,review_step,status,granted_as_de_minimis_aid,log_entry_comment,
+ handler_role,decision_text,justification_text""",
+ [
+ (
+ 200,
+ 2,
+ ApplicationStatus.ACCEPTED,
+ False,
+ None,
+ None,
+ None,
+ None,
+ ),
+ (
+ 200,
+ 3,
+ ApplicationStatus.ACCEPTED,
+ False,
+ None,
+ "manager",
+ "decision text",
+ "justification text",
+ ),
+ (
+ 200,
+ 4,
+ ApplicationStatus.ACCEPTED,
+ False,
+ None,
+ "manager",
+ "decision text",
+ "justification text",
+ ),
+ (
+ 400,
+ 2,
+ ApplicationStatus.REJECTED,
+ False,
+ "",
+ "",
+ "",
+ "",
+ ),
+ (
+ 200,
+ 3,
+ ApplicationStatus.REJECTED,
+ False,
+ "some log entry comment",
+ "handler",
+ "decision text",
+ "justification text",
+ ),
+ (
+ 400,
+ 3,
+ ApplicationStatus.REJECTED,
+ False,
+ "some log entry comment",
+ "handler",
+ "",
+ "justification text",
+ ),
+ (
+ 400,
+ 3,
+ ApplicationStatus.REJECTED,
+ False,
+ "some log entry comment",
+ "handler",
+ "decision text",
+ "",
+ ),
+ ],
+)
+def test_decision_proposal_drafting(
+ application,
+ handler_api_client,
+ response_status,
+ review_step,
+ status,
+ granted_as_de_minimis_aid,
+ log_entry_comment,
+ handler_role,
+ decision_text,
+ justification_text,
+):
+ if review_step == 4:
+ _prepare_calculation(application=application)
+
+ url = reverse("decision_proposal_draft_update")
+ response = handler_api_client.patch(
+ url,
+ {
+ "application_id": application.id,
+ "review_step": review_step,
+ "status": status,
+ "log_entry_comment": log_entry_comment,
+ "granted_as_de_minimis_aid": granted_as_de_minimis_aid,
+ "handler_role": handler_role,
+ "decision_text": decision_text,
+ "justification_text": justification_text,
+ },
+ )
+ assert response.status_code == response_status
+
+ if review_step == 4:
+ final_ahjo_text = AhjoDecisionText.objects.get(application=application)
+ assert (
+ final_ahjo_text.decision_text == f"{decision_text}\n\n{justification_text}"
+ )
diff --git a/backend/benefit/applications/tests/test_ahjo_decisions.py b/backend/benefit/applications/tests/test_ahjo_decisions.py
index 42d6d8dba7..7e2d6b38e6 100644
--- a/backend/benefit/applications/tests/test_ahjo_decisions.py
+++ b/backend/benefit/applications/tests/test_ahjo_decisions.py
@@ -3,7 +3,7 @@
import pytest
from rest_framework.reverse import reverse
-from applications.enums import DecisionProposalTemplateSectionType, DecisionType
+from applications.enums import DecisionType
from applications.models import AhjoDecisionText
from applications.services.ahjo_decision_service import (
replace_decision_template_placeholders,
@@ -14,7 +14,9 @@ def test_replace_accepted_decision_template_placeholders(
decided_application, accepted_ahjo_decision_section
):
replaced_template = replace_decision_template_placeholders(
- accepted_ahjo_decision_section.template_text, decided_application
+ accepted_ahjo_decision_section.template_decision_text,
+ accepted_ahjo_decision_section.decision_type,
+ decided_application,
)
assert decided_application.company.name in replaced_template
@@ -31,7 +33,9 @@ def test_replace_denied_decision_template_placeholders(
decided_application, denied_ahjo_decision_section
):
replaced_template = replace_decision_template_placeholders(
- denied_ahjo_decision_section.template_text, decided_application
+ denied_ahjo_decision_section.template_decision_text,
+ denied_ahjo_decision_section.decision_type,
+ decided_application,
)
assert decided_application.company.name in replaced_template
@@ -46,42 +50,24 @@ def test_anonymous_client_cannot_access_templates(
@pytest.mark.parametrize(
- "decision_type,section_type",
+ "decision_type",
[
- (
- DecisionType.ACCEPTED,
- DecisionProposalTemplateSectionType.DECISION_SECTION,
- ),
- (
- DecisionType.ACCEPTED,
- DecisionProposalTemplateSectionType.JUSTIFICATION_SECTION,
- ),
- (
- DecisionType.DENIED,
- DecisionProposalTemplateSectionType.DECISION_SECTION,
- ),
- (
- DecisionType.DENIED,
- DecisionProposalTemplateSectionType.JUSTIFICATION_SECTION,
- ),
+ (DecisionType.ACCEPTED,),
+ (DecisionType.ACCEPTED,),
+ (DecisionType.DENIED,),
+ (DecisionType.DENIED,),
],
)
def test_handler_gets_the_template_sections(
- decided_application, handler_api_client, decision_type, section_type
+ decided_application, handler_api_client, decision_type
):
url = f"""{reverse("template_section_list")}?application_id={decided_application.id}\
-&decision_type={decision_type}\
-§ion_type={section_type}"""
+&decision_type={decision_type}"""
response = handler_api_client.get(url)
assert response.status_code == 200
for template in response.data:
assert template.decision_type == decision_type
- assert template.section_type == section_type
- if (
- template.section_type
- == DecisionProposalTemplateSectionType.DECISION_SECTION
- ):
- assert decided_application.company.name in template.template_text
+ assert decided_application.company.name in template.template_decision_text
def get_decisions_list_url(application_id: uuid) -> str:
diff --git a/backend/benefit/calculator/api/v1/serializers.py b/backend/benefit/calculator/api/v1/serializers.py
index adab4e8416..d1f0d28058 100644
--- a/backend/benefit/calculator/api/v1/serializers.py
+++ b/backend/benefit/calculator/api/v1/serializers.py
@@ -28,6 +28,8 @@ class Meta:
"description_fi",
"amount",
"description_type",
+ "start_date",
+ "end_date",
]
# CalculationRows are generated by the calculator and not directly editable.
# Edit the source data instead and recalculate.
@@ -38,6 +40,8 @@ class Meta:
"description_fi",
"amount",
"description_type",
+ "start_date",
+ "end_date",
]
diff --git a/backend/benefit/calculator/rules.py b/backend/benefit/calculator/rules.py
index 5446f84036..975f58a22e 100644
--- a/backend/benefit/calculator/rules.py
+++ b/backend/benefit/calculator/rules.py
@@ -320,6 +320,7 @@ def create_rows(self):
SalaryBenefitTotalRow,
start_date=self.calculation.start_date,
end_date=self.calculation.end_date,
+ description_type=DescriptionType.DEDUCTION,
)
diff --git a/backend/benefit/docker-entrypoint.sh b/backend/benefit/docker-entrypoint.sh
index 040c0d3c0b..8382c41c0d 100755
--- a/backend/benefit/docker-entrypoint.sh
+++ b/backend/benefit/docker-entrypoint.sh
@@ -17,7 +17,9 @@ if [[ "$LOAD_FIXTURES" = "1" ]]; then
echo "Loading fixtures..."
./manage.py loaddata groups.json
if [[ "$LOAD_DEFAULT_TERMS" = "1" ]]; then
- ./manage.py loaddata default_terms.json
+ ./manage.py loaddata default_terms.json &&
+ ./manage.py loaddata test_applications.json &&
+ ./manage.py loaddata test_decision_templates.json
fi
./manage.py set_group_permissions
fi
diff --git a/backend/benefit/helsinkibenefit/urls.py b/backend/benefit/helsinkibenefit/urls.py
index f357c4331e..7baeb14562 100644
--- a/backend/benefit/helsinkibenefit/urls.py
+++ b/backend/benefit/helsinkibenefit/urls.py
@@ -13,6 +13,7 @@
from applications.api.v1 import application_batch_views, application_views
from applications.api.v1.ahjo_decision_views import (
+ DecisionProposalDraftUpdate,
DecisionProposalTemplateSectionList,
DecisionTextDetail,
DecisionTextList,
@@ -97,6 +98,11 @@
DecisionProposalTemplateSectionList.as_view(),
name="template_section_list",
),
+ path(
+ "v1/decision-proposal-drafts/",
+ DecisionProposalDraftUpdate.as_view(),
+ name="decision_proposal_draft_update",
+ ),
path("gdpr-api/v1/user/", UserUuidGDPRAPIView.as_view(), name="gdpr_v1"),
path("v1/", include((router.urls, "v1"), namespace="v1")),
path("v1/", include(applicant_app_router.urls)),
diff --git a/frontend/benefit/applicant/src/components/applications/pageContent/PageContent.tsx b/frontend/benefit/applicant/src/components/applications/pageContent/PageContent.tsx
index f89cbb4f35..071cdced11 100644
--- a/frontend/benefit/applicant/src/components/applications/pageContent/PageContent.tsx
+++ b/frontend/benefit/applicant/src/components/applications/pageContent/PageContent.tsx
@@ -209,7 +209,7 @@ const PageContent: React.FC = () => {
selectedStep={currentStep - 1}
onStepClick={(e) => e.stopPropagation()}
css={stepperCss}
- theme={theme.components.stepper}
+ theme={theme.components.stepper.black}
/>
$HeaderItem>
$PageHeader>
diff --git a/frontend/benefit/handler/browser-tests/pages/1-new-application.testcafe.ts b/frontend/benefit/handler/browser-tests/pages/1-new-application.testcafe.ts
index 58f95a4050..0b56f8f191 100644
--- a/frontend/benefit/handler/browser-tests/pages/1-new-application.testcafe.ts
+++ b/frontend/benefit/handler/browser-tests/pages/1-new-application.testcafe.ts
@@ -7,20 +7,11 @@ import fi from '../../public/locales/fi/common.json';
import { NEW_FORM_DATA as form } from '../constants/forms';
import MainIngress from '../page-model/MainIngress';
import handlerUser from '../utils/handlerUser';
+import { uploadFileAttachment } from '../utils/input';
import { getFrontendUrl } from '../utils/url.utils';
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('Create new application')
.page(url)
.beforeEach(async (t) => {
@@ -100,13 +91,9 @@ test('Fill form and submit', async (t: TestController) => {
);
await uploadFileAttachment(t, '#upload_attachment_full_application');
- await t.wait(1000);
await uploadFileAttachment(t, '#upload_attachment_employment_contract');
- await t.wait(1000);
await uploadFileAttachment(t, '#upload_attachment_education_contract');
- await t.wait(1000);
await uploadFileAttachment(t, '#upload_attachment_pay_subsidy_decision');
- await t.wait(1000);
/**
* Click through all applicant terms.
diff --git a/frontend/benefit/handler/browser-tests/pages/2-edit-application.testcafe.ts b/frontend/benefit/handler/browser-tests/pages/2-edit-application.testcafe.ts
index 683b9c6760..ea3ba9696f 100644
--- a/frontend/benefit/handler/browser-tests/pages/2-edit-application.testcafe.ts
+++ b/frontend/benefit/handler/browser-tests/pages/2-edit-application.testcafe.ts
@@ -7,31 +7,11 @@ import fi from '../../public/locales/fi/common.json';
import { EDIT_FORM_DATA as form, NEW_FORM_DATA } from '../constants/forms';
import MainIngress from '../page-model/MainIngress';
import handlerUser from '../utils/handlerUser';
+import { clearAndFill, uploadFileAttachment } from '../utils/input';
import { getFrontendUrl } from '../utils/url.utils';
const url = getFrontendUrl(`/`);
-const uploadFileAttachment = async (
- t: TestController,
- selector: string,
- filename = 'sample2.pdf'
-) => {
- await t.scrollIntoView(Selector(selector).parent(), { offsetY: -200 });
- await t.setFilesToUpload(selector, filename);
- await t.wait(100);
-};
-
-const clearAndFill = async (
- t: TestController,
- selector: string,
- value: string
-) => {
- await t.click(selector);
- await t.selectText(selector);
- await t.pressKey('delete');
- await t.typeText(selector, value ?? '');
-};
-
fixture('Edit existing application')
.page(url)
.beforeEach(async (t) => {
@@ -146,7 +126,11 @@ test('Open form and edit fields, then submit', async (t: TestController) => {
format(new Date(), DATE_FORMATS.UI_DATE)
);
- await uploadFileAttachment(t, '#upload_attachment_employment_contract');
+ await uploadFileAttachment(
+ t,
+ '#upload_attachment_employment_contract',
+ 'sample2.pdf'
+ );
// Validate form and submit
const validationButton = Selector(buttonSelector).withText(
diff --git a/frontend/benefit/handler/browser-tests/pages/4-make-decision.testcafe.ts b/frontend/benefit/handler/browser-tests/pages/4-make-decision.testcafe.ts
new file mode 100644
index 0000000000..05b87d96ef
--- /dev/null
+++ b/frontend/benefit/handler/browser-tests/pages/4-make-decision.testcafe.ts
@@ -0,0 +1,135 @@
+import { clearDataToPrintOnFailure } from '@frontend/shared/browser-tests/utils/testcafe.utils';
+import { DATE_FORMATS } from '@frontend/shared/src/utils/date.utils';
+import { addMonths, format } from 'date-fns';
+import { Selector } from 'testcafe';
+
+import fi from '../../public/locales/fi/common.json';
+import { EDIT_FORM_DATA as form } from '../constants/forms';
+import handlerUser from '../utils/handlerUser';
+import { clearAndFill } from '../utils/input';
+import { getFrontendUrl } from '../utils/url.utils';
+
+const url = getFrontendUrl(`/`);
+
+fixture('Review edited application')
+ .page(url)
+ .beforeEach(async (t) => {
+ clearDataToPrintOnFailure(t);
+ await t.useRole(handlerUser);
+ await t.navigateTo('/');
+ });
+
+test('Handler makes a favorable decision', async (t: TestController) => {
+ const applicationLink = Selector('td')
+ .withText(`${form.employee.firstName} ${form.employee.lastName}`)
+ .sibling('td')
+ .nth(0)
+ .find('a');
+
+ await t.expect(applicationLink.visible).ok();
+ await t.click(applicationLink);
+
+ // Click though the calculations
+ await t
+ .expect(Selector('main').withText(fi.calculators.salary.header).exists)
+ .ok();
+ const startDate = new Date();
+ const endDate = addMonths(startDate, 1);
+
+ // Select state aid max percentage
+ await t.click(Selector('#stateAidMaxPercentage-toggle-button'));
+ await t.click(Selector('#stateAidMaxPercentage-menu li:first-child'));
+
+ // Fill in the dates
+ await clearAndFill(t, '#endDate', format(endDate, DATE_FORMATS.UI_DATE));
+ await clearAndFill(t, '#startDate', format(startDate, DATE_FORMATS.UI_DATE));
+
+ // Click "Calculate" button
+ await t.click(
+ Selector('button').withText(fi.calculators.employment.calculate)
+ );
+
+ // Expect a "receipt" of calculation
+ await t
+ .expect(
+ Selector('[data-testid="calculation-results-total"]').withText(
+ 'Yhteensä ajanjaksolta'
+ ).exists
+ )
+ .ok();
+
+ // Click "accepted" radio
+ await t.click(Selector('label').withText(fi.review.fields.support));
+
+ // Click "Make decision" button
+ await t.click(Selector('button').withText(fi.review.actions.done));
+
+ // Click final submit inside modal prompt
+ await t.click(Selector('[data-testid="submit"]'));
+
+ // Wait for the successs notification to appear
+ await t
+ .expect(Selector('h1').withText(fi.notifications.accepted.title).exists)
+ .ok();
+});
+
+test('Handler processes favorable decision to Ahjo / Talpa', async (t: TestController) => {
+ // Select all rows and add to batch
+ await t.click(
+ Selector('li').withText(fi.applications.list.headings.accepted)
+ );
+ await t.click(Selector('button').withText('Valitse kaikki rivit'));
+ await t.click(
+ Selector('button').withText(fi.applications.list.actions.addToBatch)
+ );
+
+ // Navigate to batches
+ await t.click(Selector('a').withText(fi.header.navigation.batches));
+
+ // Click "Mark as ready for Ahjo" button
+ await t.click(
+ Selector('button').withText(fi.batches.actions.markAsReadyForAhjo)
+ );
+
+ // Confirm the action
+ await t
+ .expect(
+ Selector('[role="heading"]').withText(
+ fi.batches.notifications.statusChange.exported_ahjo_report.heading
+ ).exists
+ )
+ .ok();
+
+ // Send to nexdt step
+ await t.click(
+ Selector('button:not([disabled])').withText(
+ fi.batches.actions.markAsRegisteredToAhjo
+ )
+ );
+
+ // Click the submit button on modal prompt
+ await t.click(Selector('button').withText(fi.utility.confirm));
+ await t.click(Selector('li').withText(fi.batches.tabs.inspection));
+
+ // Type in the inspection / P2P details
+ await t.typeText(Selector('[name="decision_maker_name"]'), 'Hissun kissun');
+ await t.typeText(Selector('[name="decision_maker_title"]'), 'Vaapulavissun');
+ await t.typeText(Selector('[name="section_of_the_law"]'), '1234');
+ await t.typeText(
+ Selector('[name="expert_inspector_name"]'),
+ 'Entten Tentten'
+ );
+ await t.typeText(
+ Selector('[name="expert_inspector_title"]'),
+ 'Teelikamentten'
+ );
+ await t.typeText(Selector('[name="p2p_checker_name"]'), 'Eelin Keelin');
+
+ // Click the "Mark as ready for Talpa" button
+ await t.click(Selector('button').withText(fi.batches.actions.markToTalpa));
+ await t.click(Selector('button').withText(fi.utility.confirm));
+
+ // See if the last tab is populated with the batch
+ await t.click(Selector('li').withText(fi.batches.tabs.completion));
+ await t.expect(Selector('h2').withText('(1)').exists).ok();
+});
diff --git a/frontend/benefit/handler/browser-tests/pages/5-ahjo-process.testcafe.ts b/frontend/benefit/handler/browser-tests/pages/5-ahjo-process.testcafe.ts
new file mode 100644
index 0000000000..b6226bbc9a
--- /dev/null
+++ b/frontend/benefit/handler/browser-tests/pages/5-ahjo-process.testcafe.ts
@@ -0,0 +1,133 @@
+import { clearDataToPrintOnFailure } from '@frontend/shared/browser-tests/utils/testcafe.utils';
+import { ClientFunction, Selector } from 'testcafe';
+
+import fi from '../../public/locales/fi/common.json';
+import MainIngress from '../page-model/MainIngress';
+import handlerUserAhjo from '../utils/handlerUserAhjo';
+import { clearAndFill } from '../utils/input';
+import { getFrontendUrl } from '../utils/url.utils';
+
+const url = getFrontendUrl(`/`);
+
+fixture('Ahjo decision proposal for application')
+ .page(url)
+ .beforeEach(async (t) => {
+ clearDataToPrintOnFailure(t);
+ await t.useRole(handlerUserAhjo);
+ await t.navigateTo('/');
+ await ClientFunction(() =>
+ window.localStorage.setItem('newAhjoMode', '1')
+ )();
+ });
+
+test('Check for handling validation errors', async (t: TestController) => {
+ await ClientFunction(() => window.localStorage.setItem('newAhjoMode', '1'))();
+ const mainIngress = new MainIngress(fi.mainIngress.heading, 'h1');
+ await mainIngress.isLoaded();
+
+ // Open already created application in index page
+ const applicationLink = Selector('td')
+ .withText(`Aari Hömpömpö`)
+ .sibling('td')
+ .nth(0)
+ .find('a');
+ await t.click(applicationLink);
+
+ // // Start handling the application
+ const toastSelector = '.Toastify__toast-body[role="alert"]';
+ const buttonSelector = 'main button';
+ const handleButton = Selector(buttonSelector).withText(fi.utility.next);
+ await t.expect(handleButton.visible).ok();
+
+ // Check for empty status
+ let errorNotification = Selector(toastSelector).withText(
+ fi.review.decisionProposal.errors.fields.status
+ );
+ await t.click(handleButton);
+ await t.expect(errorNotification.visible).ok();
+
+ // Check for empty log entry
+ errorNotification = Selector(toastSelector).withText(
+ fi.review.decisionProposal.errors.fields.logEntry
+ );
+ await t.click(Selector('label').withText(fi.review.fields.noSupport));
+ await t.click(handleButton);
+ await t.expect(errorNotification.visible).ok();
+
+ // Check for calculation error
+ await clearAndFill(t, '#monthlyPay', ' ');
+ errorNotification = Selector(toastSelector).withText(
+ fi.review.decisionProposal.errors.fields.calculation
+ );
+ await t.click(handleButton);
+ await t.expect(errorNotification.visible).ok();
+});
+
+test('Open form and create a decision proposal', async (t: TestController) => {
+ await ClientFunction(() => window.localStorage.setItem('newAhjoMode', '1'))();
+ const mainIngress = new MainIngress(fi.mainIngress.heading, 'h1');
+ await mainIngress.isLoaded();
+
+ // Open already created application in index page
+ const applicationLink = Selector('td')
+ .withText(`Aari Hömpömpö`)
+ .sibling('td')
+ .nth(0)
+ .find('a');
+ await t.click(applicationLink);
+ // // Start handling the application
+ const buttonSelector = 'main button';
+ const handleButton = Selector(buttonSelector).withText(fi.utility.next);
+ await t.expect(handleButton.visible).ok();
+
+ await t.click(Selector('label').withText(fi.review.fields.support));
+ await t.click(handleButton);
+
+ await t.click(
+ Selector('label').withText(
+ fi.review.decisionProposal.role.fields.decisionMaker.handler
+ )
+ );
+
+ await t.click(
+ Selector('label').withText(
+ fi.review.decisionProposal.templates.fields.select.label
+ )
+ );
+
+ await t.click(
+ Selector('ul > li').withText('FI: Myönteisen päätöksen Päätös-osion teksti')
+ );
+
+ // Has decision text in editor
+ await t
+ .expect(Selector('[data-testid="decisionText"] .tiptap').child().count)
+ .eql(2);
+
+ // Has justification text in editor
+ await t
+ .expect(Selector('[data-testid="justificationText"] .tiptap').child().count)
+ .eql(10);
+
+ await t.click(handleButton);
+
+ await t
+ .expect(Selector('[data-testid="decision-text-preview"]').child().count)
+ .eql(2);
+
+ await t
+ .expect(
+ Selector('[data-testid="justification-text-preview"]').child().count
+ )
+ .eql(10);
+
+ await t.click(Selector(buttonSelector).withText(fi.utility.send));
+ await t.click(Selector('button').withText(fi.review.actions.accept));
+
+ await t
+ .expect(
+ Selector('h1').withText(fi.review.decisionProposal.submitted.title)
+ .visible
+ )
+ .ok();
+});
diff --git a/frontend/benefit/handler/browser-tests/utils/handlerUserAhjo.ts b/frontend/benefit/handler/browser-tests/utils/handlerUserAhjo.ts
new file mode 100644
index 0000000000..513756a8cc
--- /dev/null
+++ b/frontend/benefit/handler/browser-tests/utils/handlerUserAhjo.ts
@@ -0,0 +1,11 @@
+import { ClientFunction, Role } from 'testcafe';
+
+import { getFrontendUrl } from './url.utils';
+
+const handlerUserAhjo = Role(getFrontendUrl('/'), async (t: TestController) => {
+ // eslint-disable-next-line scanjs-rules/property_localStorage, scanjs-rules/identifier_localStorage
+ await ClientFunction(() => window.localStorage.setItem('newAhjoMode', '1'))();
+ await t.click('[data-testid="main-login-button"]');
+});
+
+export default handlerUserAhjo;
diff --git a/frontend/benefit/handler/browser-tests/utils/input.ts b/frontend/benefit/handler/browser-tests/utils/input.ts
new file mode 100644
index 0000000000..2f575404f3
--- /dev/null
+++ b/frontend/benefit/handler/browser-tests/utils/input.ts
@@ -0,0 +1,30 @@
+import { Selector } from 'testcafe';
+
+export const uploadFileAttachment = async (
+ t: TestController,
+ inputId: string,
+ filename = 'sample.pdf'
+): Promise => {
+ const filenameWithoutExtension = filename.replace(/\.\w+$/, '');
+ await t.scrollIntoView(Selector(inputId).parent(), { offsetY: -200 });
+ await t.setFilesToUpload(inputId, filename);
+ await t
+ .expect(
+ Selector(inputId)
+ .parent()
+ .parent()
+ .find(`a[aria-label^="${filenameWithoutExtension}"]`).visible
+ )
+ .ok();
+};
+
+export const clearAndFill = async (
+ t: TestController,
+ selector: string,
+ value: string
+): Promise => {
+ await t.click(selector);
+ await t.selectText(selector);
+ await t.pressKey('delete');
+ await t.typeText(selector, value ?? '');
+};
diff --git a/frontend/benefit/handler/package.json b/frontend/benefit/handler/package.json
index 60b706b575..feb2262aa0 100644
--- a/frontend/benefit/handler/package.json
+++ b/frontend/benefit/handler/package.json
@@ -18,6 +18,14 @@
"@frontend/benefit-shared": "*",
"@frontend/shared": "*",
"@sentry/browser": "^7.16.0",
+ "@tiptap/core": "^2.2.4",
+ "@tiptap/extension-document": "^2.2.4",
+ "@tiptap/extension-heading": "^2.2.4",
+ "@tiptap/extension-history": "^2.2.4",
+ "@tiptap/extension-paragraph": "^2.2.4",
+ "@tiptap/extension-text": "^2.2.4",
+ "@tiptap/pm": "^2.2.4",
+ "@tiptap/react": "^2.2.4",
"axios": "^1.6.5",
"camelcase-keys": "^7.0.2",
"date-fns": "^2.24.0",
@@ -32,9 +40,9 @@
"ibantools": "^4.3.4",
"lodash": "^4.17.21",
"next": "14.1.1",
- "next-router-mock": "0.9.12",
"next-compose-plugins": "^2.2.1",
"next-i18next": "^13.0.3",
+ "next-router-mock": "0.9.12",
"next-transpile-modules": "^9.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
diff --git a/frontend/benefit/handler/public/locales/en/common.json b/frontend/benefit/handler/public/locales/en/common.json
index 31d94cf1de..80ac194ac7 100644
--- a/frontend/benefit/handler/public/locales/en/common.json
+++ b/frontend/benefit/handler/public/locales/en/common.json
@@ -86,6 +86,7 @@
},
"ahjoButton": {
"label": "Aloita hakemuksen vieminen ahjoon",
+ "linkLabel": "Siirry Ahjoon",
"message": ""
}
},
@@ -238,9 +239,17 @@
"cancelled": "Peruttu"
},
"steps": {
- "step1": "Hae työnantaja",
- "step2": "Syötä hakemus",
- "step3": "Yhteenveto"
+ "newApplication": {
+ "step1": "Hae työnantaja",
+ "step2": "Syötä hakemus",
+ "step3": "Yhteenveto"
+ },
+ "handlingProcess": {
+ "step1": "Käsittely",
+ "step2": "Päätöksen valmistelu",
+ "step3": "Esikatsele ja lähetä"
+ },
+ "stepperHeading": "Vaihe {{currentStep}} / {{ lastStep }}:"
},
"actions": {
"saveAndContinueLater": "Tallenna luonnoksena ja sulje",
@@ -771,6 +780,9 @@
"notGranted": "Työsuhteeseen ei ole myönnetty mitään edeltävää tukea"
}
},
+ "handling": {
+ "title": "Hakemuksen käsittely"
+ },
"summary": {
"accepted": {
"title": "Hakemuksen käsittely",
@@ -820,6 +832,82 @@
"dateRange": "Helsinki-lisä kuukaudessa {{dateRange}}",
"total": "Helsinki-lisä yhteensä koko ajalta ({{months}} kk)"
}
+ },
+ "decisionProposal": {
+ "errors": {
+ "title": "Vaadittavia tietoja puuttuu",
+ "fields": {
+ "calculation": "Tarkista laskelma",
+ "status": "Puoltotieto puuttuu",
+ "logEntry": "Päätöksen perustelu puuttuu",
+ "handler": "Päättäjän rooli puuttuu",
+ "decisionText": "Päätös ei voi olla tyhjä",
+ "justificationText": "Päätöksen perustelu ei voi olla tyhjä"
+ }
+ },
+ "list": {
+ "title": "Päätösehdotuksen tiedot",
+ "text": {
+ "accepted": "Hakemus käsitellään myönteiseksi ja Helsinki-lisää myönnetään {{ months }} kuukaudelle aikavälille {{ startAndEndDate }}.",
+ "rejected": "Hakemus käsitellään kielteiseksi. Helsinki-lisää ei myönnetä hakijalle."
+ },
+ "proposalStatus": {
+ "label": "Päätösehdotus",
+ "accepted": "Myönteinen",
+ "rejected": "Kielteinen"
+ },
+ "employerName": "Hakija",
+ "employeeName": "Työllistettävä",
+ "totalAmount": "Myönnettävä tuki yhteensä",
+ "grantedAsDeMinimisAid": "Myönnetään de minimis -tukena"
+ },
+ "calculationReview": {
+ "tableCaption": "Tukijaksot",
+ "dates": "Tukijakso",
+ "duration": "Kesto",
+ "perMonth": "Tuki kuukaudessa",
+ "amount": "Tuki yhteensä"
+ },
+ "role": {
+ "title": "Valitse päättäjän rooli",
+ "fields": {
+ "decisionMaker": {
+ "label": "Päättäjän rooli",
+ "handler": "Helsinki-lisä-valmistelija",
+ "manager": "Tiimipäällikkö",
+ "tooltipText": "Hakemuksen käsittelijän ja päättäjän on oltava eri henkilöitä. Käsittelijä on henkilö, joka lähettää hakemuksen. Valitse päättäjäksi joku toinen henkilö."
+ }
+ }
+ },
+ "templates": {
+ "title": "Valitse pohja päätökselle",
+ "fields": {
+ "usedLanguage": {
+ "label": "Yhteyshenkilön asiointikieli on"
+ },
+ "select": {
+ "label": "Päätöstekstipohja",
+ "helperText": "Valitse sopiva päätöstekstipohja"
+ }
+ },
+ "missingContent": {
+ "accepted": "Myöteiset päätöstekstipohjat puuttuvat kielelle {{ language }}.",
+ "rejected": "Kielteiset päätöstekstipohjat puuttuvat kielelle {{ language }}."
+ }
+ },
+ "preview": {
+ "title": "Olet siirtämässä hakemuksen päätösjonoon.",
+ "proposalTexts": "Päätösehdotuksen teksti",
+ "decisionText": "Päätös",
+ "justificationText": "Päätöksen perustelut",
+ "staticTitle": "Avustukset työnantajille, Työllisyyspalvelut, Helsinki-lisä, ",
+ "application": "hakemus",
+ "ahjoIdentifier": "HEL 2024-000172"
+ },
+ "submitted": {
+ "title": "Päätösehdotus on lähetetty Ahjoon",
+ "text": "Viimeistele päätöskäsittely Ahjossa."
+ }
}
},
"calculators": {
@@ -993,7 +1081,10 @@
"start": "Alkamispäivä",
"end": "Päättymispäivä",
"perMonth": "/kk",
- "empty": "Tyhjä"
+ "empty": "Tyhjä",
+ "next": "Seuraava",
+ "previous": "Edellinen",
+ "send": "Lähetä"
},
"status": {
"draft": "Luonnos",
diff --git a/frontend/benefit/handler/public/locales/fi/common.json b/frontend/benefit/handler/public/locales/fi/common.json
index b5d4baa197..75dc058cb1 100644
--- a/frontend/benefit/handler/public/locales/fi/common.json
+++ b/frontend/benefit/handler/public/locales/fi/common.json
@@ -86,6 +86,7 @@
},
"ahjoButton": {
"label": "Aloita hakemuksen vieminen ahjoon",
+ "linkLabel": "Siirry Ahjoon",
"message": ""
}
},
@@ -100,7 +101,7 @@
"languages": {
"fi": "Suomi",
"sv": "Ruotsi",
- "en": "English"
+ "en": "Englanti"
},
"supportedLanguages": {
"fi": "Suomeksi",
@@ -238,9 +239,17 @@
"cancelled": "Peruttu"
},
"steps": {
- "step1": "Hae työnantaja",
- "step2": "Syötä hakemus",
- "step3": "Yhteenveto"
+ "newApplication": {
+ "step1": "Hae työnantaja",
+ "step2": "Syötä hakemus",
+ "step3": "Yhteenveto"
+ },
+ "handlingProcess": {
+ "step1": "Käsittely",
+ "step2": "Päätöksen valmistelu",
+ "step3": "Esikatsele ja lähetä"
+ },
+ "stepperHeading": "Vaihe {{currentStep}} / {{ lastStep }}:"
},
"actions": {
"saveAndContinueLater": "Tallenna luonnoksena ja sulje",
@@ -771,6 +780,9 @@
"notGranted": "Työsuhteeseen ei ole myönnetty mitään edeltävää tukea"
}
},
+ "handling": {
+ "title": "Hakemuksen käsittely"
+ },
"summary": {
"accepted": {
"title": "Hakemuksen käsittely",
@@ -820,6 +832,82 @@
"dateRange": "Helsinki-lisä kuukaudessa {{dateRange}}",
"total": "Helsinki-lisä yhteensä koko ajalta ({{months}} kk)"
}
+ },
+ "decisionProposal": {
+ "errors": {
+ "title": "Vaadittavia tietoja puuttuu",
+ "fields": {
+ "calculation": "Tarkista laskelma",
+ "status": "Puoltotieto puuttuu",
+ "logEntry": "Päätöksen perustelu puuttuu",
+ "handler": "Päättäjän rooli puuttuu",
+ "decisionText": "Päätös ei voi olla tyhjä",
+ "justificationText": "Päätöksen perustelu ei voi olla tyhjä"
+ }
+ },
+ "list": {
+ "title": "Päätösehdotuksen tiedot",
+ "text": {
+ "accepted": "Hakemus käsitellään myönteiseksi ja Helsinki-lisää myönnetään {{ months }} kuukaudelle aikavälille {{ startAndEndDate }}.",
+ "rejected": "Hakemus käsitellään kielteiseksi. Helsinki-lisää ei myönnetä hakijalle."
+ },
+ "proposalStatus": {
+ "label": "Päätösehdotus",
+ "accepted": "Myönteinen",
+ "rejected": "Kielteinen"
+ },
+ "employerName": "Hakija",
+ "employeeName": "Työllistettävä",
+ "totalAmount": "Myönnettävä tuki yhteensä",
+ "grantedAsDeMinimisAid": "Myönnetään de minimis -tukena"
+ },
+ "calculationReview": {
+ "tableCaption": "Tukijaksot",
+ "dates": "Tukijakso",
+ "duration": "Kesto",
+ "perMonth": "Tuki kuukaudessa",
+ "amount": "Tuki yhteensä"
+ },
+ "role": {
+ "title": "Valitse päättäjän rooli",
+ "fields": {
+ "decisionMaker": {
+ "label": "Päättäjän rooli",
+ "handler": "Helsinki-lisä-valmistelija",
+ "manager": "Tiimipäällikkö",
+ "tooltipText": "Hakemuksen käsittelijän ja päättäjän on oltava eri henkilöitä. Käsittelijä on henkilö, joka lähettää hakemuksen. Valitse päättäjäksi joku toinen henkilö."
+ }
+ }
+ },
+ "templates": {
+ "title": "Valitse pohja päätökselle",
+ "fields": {
+ "usedLanguage": {
+ "label": "Yhteyshenkilön asiointikieli on"
+ },
+ "select": {
+ "label": "Päätöstekstipohja",
+ "helperText": "Valitse sopiva päätöstekstipohja"
+ }
+ },
+ "missingContent": {
+ "accepted": "Myönteiset päätöstekstipohjat puuttuvat kielelle {{ language }}.",
+ "rejected": "Kielteiset päätöstekstipohjat puuttuvat kielelle {{ language }}."
+ }
+ },
+ "preview": {
+ "title": "Olet siirtämässä hakemuksen päätösjonoon.",
+ "proposalTexts": "Päätösehdotuksen teksti",
+ "decisionText": "Päätös",
+ "justificationText": "Päätöksen perustelut",
+ "staticTitle": "Avustukset työnantajille, Työllisyyspalvelut, Helsinki-lisä, ",
+ "application": "hakemus",
+ "ahjoIdentifier": "HEL 2024-000172"
+ },
+ "submitted": {
+ "title": "Päätösehdotus on lähetetty Ahjoon",
+ "text": "Viimeistele päätöskäsittely Ahjossa."
+ }
}
},
"calculators": {
@@ -993,7 +1081,10 @@
"start": "Alkamispäivä",
"end": "Päättymispäivä",
"perMonth": "/kk",
- "empty": "Tyhjä"
+ "empty": "Tyhjä",
+ "next": "Seuraava",
+ "previous": "Edellinen",
+ "send": "Lähetä"
},
"status": {
"draft": "Luonnos",
diff --git a/frontend/benefit/handler/public/locales/sv/common.json b/frontend/benefit/handler/public/locales/sv/common.json
index 31d94cf1de..80ac194ac7 100644
--- a/frontend/benefit/handler/public/locales/sv/common.json
+++ b/frontend/benefit/handler/public/locales/sv/common.json
@@ -86,6 +86,7 @@
},
"ahjoButton": {
"label": "Aloita hakemuksen vieminen ahjoon",
+ "linkLabel": "Siirry Ahjoon",
"message": ""
}
},
@@ -238,9 +239,17 @@
"cancelled": "Peruttu"
},
"steps": {
- "step1": "Hae työnantaja",
- "step2": "Syötä hakemus",
- "step3": "Yhteenveto"
+ "newApplication": {
+ "step1": "Hae työnantaja",
+ "step2": "Syötä hakemus",
+ "step3": "Yhteenveto"
+ },
+ "handlingProcess": {
+ "step1": "Käsittely",
+ "step2": "Päätöksen valmistelu",
+ "step3": "Esikatsele ja lähetä"
+ },
+ "stepperHeading": "Vaihe {{currentStep}} / {{ lastStep }}:"
},
"actions": {
"saveAndContinueLater": "Tallenna luonnoksena ja sulje",
@@ -771,6 +780,9 @@
"notGranted": "Työsuhteeseen ei ole myönnetty mitään edeltävää tukea"
}
},
+ "handling": {
+ "title": "Hakemuksen käsittely"
+ },
"summary": {
"accepted": {
"title": "Hakemuksen käsittely",
@@ -820,6 +832,82 @@
"dateRange": "Helsinki-lisä kuukaudessa {{dateRange}}",
"total": "Helsinki-lisä yhteensä koko ajalta ({{months}} kk)"
}
+ },
+ "decisionProposal": {
+ "errors": {
+ "title": "Vaadittavia tietoja puuttuu",
+ "fields": {
+ "calculation": "Tarkista laskelma",
+ "status": "Puoltotieto puuttuu",
+ "logEntry": "Päätöksen perustelu puuttuu",
+ "handler": "Päättäjän rooli puuttuu",
+ "decisionText": "Päätös ei voi olla tyhjä",
+ "justificationText": "Päätöksen perustelu ei voi olla tyhjä"
+ }
+ },
+ "list": {
+ "title": "Päätösehdotuksen tiedot",
+ "text": {
+ "accepted": "Hakemus käsitellään myönteiseksi ja Helsinki-lisää myönnetään {{ months }} kuukaudelle aikavälille {{ startAndEndDate }}.",
+ "rejected": "Hakemus käsitellään kielteiseksi. Helsinki-lisää ei myönnetä hakijalle."
+ },
+ "proposalStatus": {
+ "label": "Päätösehdotus",
+ "accepted": "Myönteinen",
+ "rejected": "Kielteinen"
+ },
+ "employerName": "Hakija",
+ "employeeName": "Työllistettävä",
+ "totalAmount": "Myönnettävä tuki yhteensä",
+ "grantedAsDeMinimisAid": "Myönnetään de minimis -tukena"
+ },
+ "calculationReview": {
+ "tableCaption": "Tukijaksot",
+ "dates": "Tukijakso",
+ "duration": "Kesto",
+ "perMonth": "Tuki kuukaudessa",
+ "amount": "Tuki yhteensä"
+ },
+ "role": {
+ "title": "Valitse päättäjän rooli",
+ "fields": {
+ "decisionMaker": {
+ "label": "Päättäjän rooli",
+ "handler": "Helsinki-lisä-valmistelija",
+ "manager": "Tiimipäällikkö",
+ "tooltipText": "Hakemuksen käsittelijän ja päättäjän on oltava eri henkilöitä. Käsittelijä on henkilö, joka lähettää hakemuksen. Valitse päättäjäksi joku toinen henkilö."
+ }
+ }
+ },
+ "templates": {
+ "title": "Valitse pohja päätökselle",
+ "fields": {
+ "usedLanguage": {
+ "label": "Yhteyshenkilön asiointikieli on"
+ },
+ "select": {
+ "label": "Päätöstekstipohja",
+ "helperText": "Valitse sopiva päätöstekstipohja"
+ }
+ },
+ "missingContent": {
+ "accepted": "Myöteiset päätöstekstipohjat puuttuvat kielelle {{ language }}.",
+ "rejected": "Kielteiset päätöstekstipohjat puuttuvat kielelle {{ language }}."
+ }
+ },
+ "preview": {
+ "title": "Olet siirtämässä hakemuksen päätösjonoon.",
+ "proposalTexts": "Päätösehdotuksen teksti",
+ "decisionText": "Päätös",
+ "justificationText": "Päätöksen perustelut",
+ "staticTitle": "Avustukset työnantajille, Työllisyyspalvelut, Helsinki-lisä, ",
+ "application": "hakemus",
+ "ahjoIdentifier": "HEL 2024-000172"
+ },
+ "submitted": {
+ "title": "Päätösehdotus on lähetetty Ahjoon",
+ "text": "Viimeistele päätöskäsittely Ahjossa."
+ }
}
},
"calculators": {
@@ -993,7 +1081,10 @@
"start": "Alkamispäivä",
"end": "Päättymispäivä",
"perMonth": "/kk",
- "empty": "Tyhjä"
+ "empty": "Tyhjä",
+ "next": "Seuraava",
+ "previous": "Edellinen",
+ "send": "Lähetä"
},
"status": {
"draft": "Luonnos",
diff --git a/frontend/benefit/handler/src/components/applicationForm/companySearch/CompanySearch.tsx b/frontend/benefit/handler/src/components/applicationForm/companySearch/CompanySearch.tsx
index 3b4c324617..aa183abd24 100644
--- a/frontend/benefit/handler/src/components/applicationForm/companySearch/CompanySearch.tsx
+++ b/frontend/benefit/handler/src/components/applicationForm/companySearch/CompanySearch.tsx
@@ -59,6 +59,7 @@ const CompanySearch: React.FC = () => {
>
{companies.map(({ name, business_id: businessId }) => (
{
onSelectCompany()}
css={`
margin-top: ${theme.spacing.l};
diff --git a/frontend/benefit/handler/src/components/applicationHeader/ApplicationHeader.sc.ts b/frontend/benefit/handler/src/components/applicationHeader/ApplicationHeader.sc.ts
index fae6584590..7b47ef6ebf 100644
--- a/frontend/benefit/handler/src/components/applicationHeader/ApplicationHeader.sc.ts
+++ b/frontend/benefit/handler/src/components/applicationHeader/ApplicationHeader.sc.ts
@@ -1,13 +1,16 @@
import styled from 'styled-components';
export const $Wrapper = styled.div`
- padding: ${(props) => props.theme.spacing.xs2} 0;
+ margin: 0 0 ${(props) => props.theme.spacing.xl};
+`;
+
+export const $Background = styled.div`
background-color: ${(props) => props.theme.colors.coatOfArms};
- color: ${(props) => props.theme.colors.white};
- margin-bottom: ${(props) => props.theme.spacing.xl};
+ padding: ${(props) => props.theme.spacing.xs2} 0;
`;
export const $InnerWrapper = styled.div`
+ color: ${(props) => props.theme.colors.white};
display: flex;
justify-content: space-between;
align-items: center;
diff --git a/frontend/benefit/handler/src/components/applicationHeader/ApplicationHeader.tsx b/frontend/benefit/handler/src/components/applicationHeader/ApplicationHeader.tsx
index 9ef799d40f..ffbfb0de1e 100644
--- a/frontend/benefit/handler/src/components/applicationHeader/ApplicationHeader.tsx
+++ b/frontend/benefit/handler/src/components/applicationHeader/ApplicationHeader.tsx
@@ -1,13 +1,21 @@
import StatusLabel from 'benefit/handler/components/statusLabel/StatusLabel';
import { APPLICATION_STATUSES } from 'benefit-shared/constants';
import { Application } from 'benefit-shared/types/application';
+import { IconLockOpen } from 'hds-react';
import { useTranslation } from 'next-i18next';
import * as React from 'react';
import Container from 'shared/components/container/Container';
import { getFullName } from 'shared/utils/application.utils';
-import { formatDate } from 'shared/utils/date.utils';
+import { convertToUIDateFormat, formatDate } from 'shared/utils/date.utils';
+import {
+ getLocalStorageItem,
+ removeLocalStorageItem,
+ setLocalStorageItem,
+} from 'shared/utils/localstorage.utils';
+import { $InfoNeededBar } from '../applicationReview/ApplicationReview.sc';
import {
+ $Background,
$Col,
$InnerWrapper,
$ItemHeader,
@@ -18,6 +26,20 @@ import {
type ApplicationReviewProps = { data: Application };
+const toggleNewAhjoMode = (): void => {
+ // eslint-disable-next-line no-alert
+ const confirm = window.confirm(
+ 'Kokeile Ahjo-integraation käyttöliittymää? Vain testiympäristöihin, älä käytä tuotannossa!'
+ );
+ if (!confirm) return;
+ if (getLocalStorageItem('newAhjoMode') !== '1') {
+ setLocalStorageItem('newAhjoMode', '1');
+ } else {
+ removeLocalStorageItem('newAhjoMode');
+ }
+ window.location.reload();
+};
+
const ApplicationHeader: React.FC = ({ data }) => {
const { t } = useTranslation();
const translationBase = 'common:applications.list.columns';
@@ -37,45 +59,71 @@ const ApplicationHeader: React.FC = ({ data }) => {
return (
<$Wrapper>
-
- <$InnerWrapper>
- <$Col>
- <$ItemWrapper>
- <$ItemHeader>{t(`${translationBase}.companyName`)}$ItemHeader>
- <$ItemValue>{data.company?.name}$ItemValue>
- $ItemWrapper>
- <$ItemWrapper>
- <$ItemHeader>{t(`${translationBase}.companyId`)}$ItemHeader>
- <$ItemValue>{data.company?.businessId}$ItemValue>
- $ItemWrapper>
- <$ItemWrapper>
- <$ItemHeader>
- {t(`${translationBase}.applicationNum`)}
- $ItemHeader>
- <$ItemValue>{data.applicationNumber}$ItemValue>
- $ItemWrapper>
- <$ItemWrapper>
- <$ItemHeader>{t(`${translationBase}.employeeName`)}$ItemHeader>
- <$ItemValue>{employeeName}$ItemValue>
- $ItemWrapper>
- {handlerName && (
+ <$Background>
+
+ <$InnerWrapper>
+ <$Col>
+ <$ItemWrapper>
+ <$ItemHeader>{t(`${translationBase}.companyName`)}$ItemHeader>
+ <$ItemValue>{data.company?.name}$ItemValue>
+ $ItemWrapper>
+ <$ItemWrapper>
+ <$ItemHeader>{t(`${translationBase}.companyId`)}$ItemHeader>
+ <$ItemValue>{data.company?.businessId}$ItemValue>
+ $ItemWrapper>
+ <$ItemWrapper>
+ <$ItemHeader>
+ {t(`${translationBase}.applicationNum`)}
+ $ItemHeader>
+ <$ItemValue>{data.applicationNumber}$ItemValue>
+ $ItemWrapper>
+ <$ItemWrapper>
+ <$ItemHeader>
+ {t(`${translationBase}.employeeName`)}
+ $ItemHeader>
+ <$ItemValue>{employeeName}$ItemValue>
+ $ItemWrapper>
+ {handlerName && (
+ <$ItemWrapper>
+ <$ItemHeader>
+ {t(`${translationBase}.handlerName`)}
+ $ItemHeader>
+ <$ItemValue>{handlerName}$ItemValue>
+ $ItemWrapper>
+ )}
+ <$ItemWrapper>
+ <$ItemHeader>{t(`${translationBase}.submittedAt`)}$ItemHeader>
+ <$ItemValue>
+ {data.submittedAt && formatDate(new Date(data.submittedAt))}
+ $ItemValue>
+ $ItemWrapper>
<$ItemWrapper>
- <$ItemHeader>{t(`${translationBase}.handlerName`)}$ItemHeader>
- <$ItemValue>{handlerName}$ItemValue>
+
+ Ahjo-kokeilu
+
+ {getLocalStorageItem('newAhjoMode') ? 'pois' : 'päälle'}
+
$ItemWrapper>
- )}
- <$ItemWrapper>
- <$ItemHeader>{t(`${translationBase}.submittedAt`)}$ItemHeader>
- <$ItemValue>
- {data.submittedAt && formatDate(new Date(data.submittedAt))}
- $ItemValue>
- $ItemWrapper>
- $Col>
- <$Col>
-
- $Col>
- $InnerWrapper>
-
+ $Col>
+ <$Col>
+
+ $Col>
+ $InnerWrapper>
+
+ $Background>
+
+ {data.status === APPLICATION_STATUSES.INFO_REQUIRED && (
+ <$InfoNeededBar>
+ {t(`common:review.fields.editEndDate`, {
+ date: convertToUIDateFormat(data.additionalInformationNeededBy),
+ })}
+
+ $InfoNeededBar>
+ )}
$Wrapper>
);
};
diff --git a/frontend/benefit/handler/src/components/applicationList/ApplicationList.tsx b/frontend/benefit/handler/src/components/applicationList/ApplicationList.tsx
index 6d85c782a9..2cbe3a45c7 100644
--- a/frontend/benefit/handler/src/components/applicationList/ApplicationList.tsx
+++ b/frontend/benefit/handler/src/components/applicationList/ApplicationList.tsx
@@ -1,3 +1,5 @@
+import 'react-loading-skeleton/dist/skeleton.css';
+
import { ALL_APPLICATION_STATUSES, ROUTES } from 'benefit/handler/constants';
import {
ApplicationListTableColumns,
@@ -5,14 +7,9 @@ import {
} from 'benefit/handler/types/applicationList';
import { APPLICATION_STATUSES } from 'benefit-shared/constants';
import { ApplicationListItemData } from 'benefit-shared/types/application';
-import {
- IconSpeechbubbleText,
- LoadingSpinner,
- StatusLabel,
- Table,
- Tag,
-} from 'hds-react';
+import { IconSpeechbubbleText, StatusLabel, 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,
@@ -205,8 +202,17 @@ const ApplicationList: React.FC = ({
if (isLoading) {
return (
<>
- {heading && <$Heading>{`${heading}`}$Heading>}
-
+ {heading && (
+ <$Heading
+ css={{ marginBottom: theme.spacing.xs }}
+ >{`${heading}`}$Heading>
+ )}
+
+
>
);
}
diff --git a/frontend/benefit/handler/src/components/applicationReview/ApplicationReview.sc.ts b/frontend/benefit/handler/src/components/applicationReview/ApplicationReview.sc.ts
index 346bb0e6ff..088a71945d 100644
--- a/frontend/benefit/handler/src/components/applicationReview/ApplicationReview.sc.ts
+++ b/frontend/benefit/handler/src/components/applicationReview/ApplicationReview.sc.ts
@@ -15,6 +15,12 @@ type TabButtonProps = {
active?: boolean;
};
+export const $ApplicationReview = styled.div`
+ hr {
+ border: 1px solid ${(props) => props.theme.colors.silver};
+ }
+`;
+
export const $MainHeader = styled.h1`
font-size: ${(props) => props.theme.fontSize.heading.m};
`;
@@ -126,3 +132,17 @@ export const $InfoNeededBar = styled.div`
padding-left: ${(props) => props.theme.spacing.xs};
}
`;
+
+export const $CalculationReviewTableWrapper = styled.div`
+ max-width: 714px;
+ margin-top: ${(props) => props.theme.spacing.xs};
+
+ caption {
+ font-weight: 500;
+ }
+
+ table > tbody > tr:last-child td {
+ background-color: ${(props) => props.theme.colors.coatOfArmsLight};
+ font-weight: 500;
+ }
+`;
diff --git a/frontend/benefit/handler/src/components/applicationReview/ApplicationReview.tsx b/frontend/benefit/handler/src/components/applicationReview/ApplicationReview.tsx
index d6aa778545..2e615d3895 100644
--- a/frontend/benefit/handler/src/components/applicationReview/ApplicationReview.tsx
+++ b/frontend/benefit/handler/src/components/applicationReview/ApplicationReview.tsx
@@ -1,135 +1,151 @@
import ApplicationHeader from 'benefit/handler/components/applicationHeader/ApplicationHeader';
import { HANDLED_STATUSES } from 'benefit/handler/constants';
-import ReviewStateContext from 'benefit/handler/context/ReviewStateContext';
-import {
- APPLICATION_ORIGINS,
- APPLICATION_STATUSES,
-} from 'benefit-shared/constants';
-import { IconLockOpen, LoadingSpinner } from 'hds-react';
+import { useDetermineAhjoMode } from 'benefit/handler/hooks/useDetermineAhjoMode';
+import { APPLICATION_STATUSES } from 'benefit-shared/constants';
+import { ErrorData } from 'benefit-shared/types/common';
+import { useRouter } from 'next/router';
import * as React from 'react';
+import LoadingSkeleton from 'react-loading-skeleton';
+import { useQueryClient } from 'react-query';
import Container from 'shared/components/container/Container';
+import {
+ $Grid,
+ $GridCell,
+} from 'shared/components/forms/section/FormSection.sc';
import StickyActionBar from 'shared/components/stickyActionBar/StickyActionBar';
import { $StickyBarSpacing } from 'shared/components/stickyActionBar/StickyActionBar.sc';
-import { convertToUIDateFormat } from 'shared/utils/date.utils';
+import theme from 'shared/styles/theme';
+import { useApplicationStepper } from '../../hooks/applicationHandling/useHandlingStepper';
import HandlingApplicationActions from './actions/handlingApplicationActions/HandlingApplicationActions';
+import HandlingApplicationActionsAhjo from './actions/handlingApplicationActions/HandlingApplicationActionsAhjo';
import ReceivedApplicationActions from './actions/receivedApplicationActions/ReceivedApplicationActions';
-import ApplicationProcessingView from './applicationProcessingView/AplicationProcessingView';
-import { $InfoNeededBar } from './ApplicationReview.sc';
-import BenefitView from './benefitView/BenefitView';
-import CompanyInfoView from './companyInfoView/CompanyInfoView';
-import ConsentView from './consentView/ConsentView';
-import ContactPersonView from './contactPersonView/ContactPersonView';
-import CoOperationNegotiationsView from './coOperationNegotiationsView/CoOperationNegotiationsView';
-import DeminimisView from './deminimisView/DeminimisView';
-import EmployeeView from './employeeView/EmployeeView';
-import EmploymentView from './employmentView/EmpoymentView';
-import ArchivedView from './handledView/archivedView/ArchivedView';
-import HandledView from './handledView/HandledView';
+import { $ApplicationReview } from './ApplicationReview.sc';
+import ApplicationReviewStep1 from './handlingView/HandlingStep1';
+import ApplicationReviewStep2 from './handlingView/HandlingStep2';
+import ApplicationReviewStep3 from './handlingView/HandlingStep3';
+import ApplicationStepper from './handlingView/HandlingStepper';
import NotificationView from './notificationView/NotificationView';
-import PaperView from './paperView/PaperView';
-import SalaryBenefitCalculatorView from './salaryBenefitCalculatorView/SalaryBenefitCalculatorView';
import { useApplicationReview } from './useApplicationReview';
const ApplicationReview: React.FC = () => {
- const {
- application,
- handledApplication,
- isLoading,
+ const { application, isLoading, t } = useApplicationReview();
+
+ const isNewAhjoMode = useDetermineAhjoMode();
+
+ const { stepState, stepperDispatch } = useApplicationStepper(
+ application?.id ?? '',
t,
- isUploading,
- handleUpload,
- reviewState,
- handleUpdateReviewState,
- } = useApplicationReview();
+ useQueryClient()
+ );
+
+ const [isRecalculationRequired, setIsRecalculationRequired] =
+ React.useState(false);
+ const [calculationsErrors, setCalculationErrors] = React.useState<
+ ErrorData | undefined | null
+ >();
+
+ const router = useRouter();
if (isLoading) {
return (
-
-
-
+ <>
+
+
+
+ <$Grid>
+ <$GridCell $colSpan={12}>
+
+ $GridCell>
+ <$GridCell $colSpan={12}>
+
+ $GridCell>
+ <$GridCell $colSpan={12}>
+
+ $GridCell>
+ <$GridCell $colSpan={12}>
+
+ $GridCell>
+ $Grid>
+
+ >
);
}
- if (handledApplication?.status === application.status) {
+ if (router.query?.action === 'submit') {
return ;
}
return (
- <>
+ <$ApplicationReview>
-
- {application.status === APPLICATION_STATUSES.INFO_REQUIRED && (
- <$InfoNeededBar>
- {t(`common:review.fields.editEndDate`, {
- date: convertToUIDateFormat(
- application.additionalInformationNeededBy
- ),
- })}
-
- $InfoNeededBar>
+
+ {isNewAhjoMode &&
+ [
+ APPLICATION_STATUSES.HANDLING,
+ APPLICATION_STATUSES.INFO_REQUIRED,
+ ].includes(application.status) && (
+
)}
-
- {application.applicationOrigin === APPLICATION_ORIGINS.HANDLER && (
-
- )}
-
-
-
-
-
-
-
-
+ )}
+
+ {stepState.activeStepIndex === 1 && (
+
+ )}
+ {stepState.activeStepIndex === 2 && (
+
+ )}
+
+
+ {application.status === APPLICATION_STATUSES.RECEIVED && (
+
- {application.status === APPLICATION_STATUSES.HANDLING && (
- <>
-
-
- >
- )}
- {application.status &&
- HANDLED_STATUSES.includes(application.status) && (
-
- )}
- {application.archived && }
-
-
- {application.status === APPLICATION_STATUSES.RECEIVED && (
-
)}
- {(application.status === APPLICATION_STATUSES.HANDLING ||
- application.status === APPLICATION_STATUSES.INFO_REQUIRED ||
- (application.status &&
- HANDLED_STATUSES.includes(application.status))) && (
-
)}
-
- <$StickyBarSpacing />
-
- >
+
+ <$StickyBarSpacing />
+ $ApplicationReview>
);
};
diff --git a/frontend/benefit/handler/src/components/applicationReview/CalculationReview.tsx b/frontend/benefit/handler/src/components/applicationReview/CalculationReview.tsx
new file mode 100644
index 0000000000..de64341598
--- /dev/null
+++ b/frontend/benefit/handler/src/components/applicationReview/CalculationReview.tsx
@@ -0,0 +1,224 @@
+import AppContext from 'benefit/handler/context/AppContext';
+import { ApplicationListTableColumns } from 'benefit/handler/types/applicationList';
+import { APPLICATION_STATUSES } from 'benefit-shared/constants';
+import { Application, Row } from 'benefit-shared/types/application';
+import { IconCheckCircleFill, IconCrossCircleFill, Table } from 'hds-react';
+import clone from 'lodash/clone';
+import { TFunction, useTranslation } from 'next-i18next';
+import * as React from 'react';
+import Heading from 'shared/components/forms/heading/Heading';
+import { $GridCell } from 'shared/components/forms/section/FormSection.sc';
+import theme from 'shared/styles/theme';
+import { convertToUIDateFormat, diffMonths } from 'shared/utils/date.utils';
+import { formatFloatToCurrency } from 'shared/utils/string.utils';
+
+import { $HorizontalList } from '../table/TableExtras.sc';
+import { $CalculationReviewTableWrapper } from './ApplicationReview.sc';
+
+type ApplicationReviewStepProps = {
+ application: Application;
+};
+
+type BenefitRow = {
+ id: string;
+ dates: string;
+ amount: string;
+ amountNumber: string;
+ perMonth: string;
+ duration: number;
+ startDate: string;
+ endDate: string;
+};
+
+const createBenefitRow = (): BenefitRow =>
+ clone({
+ id: '',
+ dates: '',
+ amount: '',
+ amountNumber: '',
+ perMonth: '',
+ duration: 0,
+ startDate: '',
+ endDate: '',
+ });
+
+const getTableCols = (t: TFunction): ApplicationListTableColumns[] => [
+ {
+ key: 'dates',
+ headerName: t('common:review.decisionProposal.calculationReview.dates'),
+ },
+ {
+ key: 'duration',
+ headerName: t('common:review.decisionProposal.calculationReview.duration'),
+ },
+ {
+ key: 'perMonth',
+ headerName: t('common:review.decisionProposal.calculationReview.perMonth'),
+ },
+ {
+ key: 'amount',
+ headerName: t('common:review.decisionProposal.calculationReview.amount'),
+ },
+];
+
+const CalculationReview: React.FC = ({
+ application,
+}) => {
+ const { company, employee, calculation, decisionProposalDraft } = application;
+ const filteredData: Row[] = calculation.rows.filter((row) =>
+ ['helsinki_benefit_monthly_eur', 'helsinki_benefit_sub_total_eur'].includes(
+ row.rowType
+ )
+ );
+ const { t } = useTranslation();
+ const { handledApplication } = React.useContext(AppContext);
+
+ let tableRow = createBenefitRow();
+
+ const tableRows = filteredData.reduce((acc: BenefitRow[], row: Row) => {
+ tableRow.id = row.id;
+ if (row.rowType === 'helsinki_benefit_monthly_eur') {
+ tableRow.perMonth = `${formatFloatToCurrency(row.amount)} / kk`;
+ }
+ if (row.rowType === 'helsinki_benefit_sub_total_eur') {
+ tableRow.dates = `${convertToUIDateFormat(
+ row.startDate
+ )} - ${convertToUIDateFormat(row.endDate)}`;
+
+ tableRow.endDate = convertToUIDateFormat(row.endDate);
+ tableRow.startDate = convertToUIDateFormat(row.startDate);
+
+ tableRow.duration = diffMonths(
+ new Date(row.endDate),
+ new Date(row.startDate)
+ );
+
+ tableRow.amount = formatFloatToCurrency(row.amount);
+ tableRow.amountNumber = row.amount;
+ acc.push(tableRow);
+ tableRow = createBenefitRow();
+ }
+ return acc;
+ }, []);
+
+ const totalSum = tableRows.reduce(
+ (acc: number, cur: BenefitRow) => acc + parseFloat(cur.amountNumber),
+ 0
+ );
+
+ if (tableRows.length > 0)
+ tableRows.push({
+ ...createBenefitRow(),
+ id: 'table-footer',
+ dates: `${tableRows.at(0).startDate} - ${tableRows.at(-1).endDate}`,
+ duration: tableRows.reduce(
+ (acc: number, cur: BenefitRow) => acc + cur.duration,
+ 0
+ ),
+ amount: formatFloatToCurrency(totalSum),
+ });
+
+ return (
+ <>
+ <$GridCell $colSpan={12}>
+
+ {handledApplication.status === APPLICATION_STATUSES.ACCEPTED && (
+
+ {t('common:review.decisionProposal.list.text.accepted', {
+ months: tableRows.at(-1)?.duration,
+ startAndEndDate: `${tableRows.at(-1)?.dates}`,
+ })}
+
+ )}
+ {handledApplication.status === APPLICATION_STATUSES.REJECTED && (
+ {t('common:review.decisionProposal.list.text.rejected')}
+ )}
+
+ $GridCell>
+
+ <$GridCell $colSpan={12}>
+ <$HorizontalList css="padding-left: 0;">
+
+
+ {t('common:review.decisionProposal.list.proposalStatus.label')}
+
+
+ {decisionProposalDraft.status ===
+ APPLICATION_STATUSES.ACCEPTED ? (
+ <>
+ {' '}
+ {t(
+ 'common:review.decisionProposal.list.proposalStatus.accepted'
+ )}
+ >
+ ) : (
+ <>
+ {' '}
+ {t(
+ 'common:review.decisionProposal.list.proposalStatus.rejected'
+ )}
+ >
+ )}
+
+
+
+
{t('common:review.decisionProposal.list.employerName')}
+ {company.name}
+
+ {decisionProposalDraft.status === APPLICATION_STATUSES.REJECTED && (
+
+
{t('common:review.decisionProposal.list.employeeName')}
+
+ {employee.firstName} {employee.lastName}
+
+
+ )}
+ {decisionProposalDraft.status === APPLICATION_STATUSES.ACCEPTED && (
+ <>
+
+
{t('common:review.decisionProposal.list.totalAmount')}
+ {formatFloatToCurrency(totalSum)}
+
+
+
+ {t(
+ 'common:review.decisionProposal.list.grantedAsDeMinimisAid'
+ )}
+
+
+ {decisionProposalDraft.grantedAsDeMinimisAid
+ ? t('common:utility.yes')
+ : t('common:utility.no')}
+
+
+ >
+ )}
+ $HorizontalList>
+ $GridCell>
+ {handledApplication.status === APPLICATION_STATUSES.ACCEPTED && (
+ <$GridCell $colSpan={12}>
+
+
+ <$CalculationReviewTableWrapper>
+
+ $CalculationReviewTableWrapper>
+ $GridCell>
+ )}
+ >
+ );
+};
+
+export default CalculationReview;
diff --git a/frontend/benefit/handler/src/components/applicationReview/actions/handlingApplicationActions/HandlingApplicationActionsAhjo.tsx b/frontend/benefit/handler/src/components/applicationReview/actions/handlingApplicationActions/HandlingApplicationActionsAhjo.tsx
new file mode 100644
index 0000000000..aeeb4f14e3
--- /dev/null
+++ b/frontend/benefit/handler/src/components/applicationReview/actions/handlingApplicationActions/HandlingApplicationActionsAhjo.tsx
@@ -0,0 +1,345 @@
+import Sidebar from 'benefit/handler/components/sidebar/Sidebar';
+import { HANDLED_STATUSES } from 'benefit/handler/constants';
+import { APPLICATION_STATUSES } from 'benefit-shared/constants';
+import { Application } from 'benefit-shared/types/application';
+import {
+ Button,
+ IconArrowLeft,
+ IconArrowRight,
+ IconInfoCircle,
+ IconLock,
+ IconPen,
+ IconTrash,
+} from 'hds-react';
+import noop from 'lodash/noop';
+import { useRouter } from 'next/router';
+import * as React from 'react';
+import Modal from 'shared/components/modal/Modal';
+import showErrorToast from 'shared/components/toast/show-error-toast';
+import theme from 'shared/styles/theme';
+import { focusAndScroll } from 'shared/utils/dom.utils';
+
+import useDecisionProposalDraftMutation from '../../../../hooks/applicationHandling/useDecisionProposalDraftMutation';
+import {
+ StepActionType,
+ StepStateType,
+} from '../../../../hooks/applicationHandling/useHandlingStepper';
+import EditAction from '../editAction/EditAction';
+import CancelModalContent from './CancelModalContent/CancelModalContent';
+import DoneModalContent from './DoneModalContent/DoneModalContent';
+import {
+ $Column,
+ $CustomNotesActions,
+ $Wrapper,
+} from './HandlingApplicationActions.sc';
+import { useHandlingApplicationActions } from './useHandlingApplicationActions';
+
+export type Props = {
+ application: Application;
+ stepperDispatch: React.Dispatch;
+ stepState: StepStateType;
+ 'data-testid'?: string;
+ isRecalculationRequired: boolean;
+ isCalculationsErrors: boolean;
+};
+
+const HandlingApplicationActions: React.FC = ({
+ application,
+ stepperDispatch,
+ stepState,
+ 'data-testid': dataTestId,
+ isRecalculationRequired,
+ isCalculationsErrors,
+}) => {
+ const {
+ t,
+ toggleMessagesDrawerVisiblity,
+ openDialog,
+ closeDialog,
+ closeDoneDialog,
+ handleCancel,
+ isMessagesDrawerVisible,
+ translationsBase,
+ isConfirmationModalOpen,
+ isDoneConfirmationModalOpen,
+ handledApplication,
+ onDoneConfirmation,
+ } = useHandlingApplicationActions(application);
+
+ const lastStep =
+ stepState.activeStepIndex === Number(stepState.steps?.length) - 1;
+ const router = useRouter();
+ const {
+ data,
+ mutate: updateApplication,
+ isError,
+ } = useDecisionProposalDraftMutation(application);
+
+ const [isSavingAndClosing, setIsSavingAndClosing] = React.useState(false);
+
+ const effectSaveAndClose = (): void => {
+ if (
+ data?.review_step === stepState.activeStepIndex + 1 &&
+ isSavingAndClosing
+ ) {
+ setIsSavingAndClosing(false);
+ void router.push('/');
+ }
+ };
+
+ const effectReviewStepChange = (): void => {
+ if (data?.review_step) {
+ stepperDispatch({
+ type: 'completeStep',
+ payload: data.review_step - 2,
+ });
+ }
+ };
+
+ const effectApplicationStatusChange = (): void => {
+ if (
+ [APPLICATION_STATUSES.ACCEPTED, APPLICATION_STATUSES.REJECTED].includes(
+ application.status
+ ) &&
+ stepState.activeStepIndex === 2
+ ) {
+ router.query.action = 'submit';
+ void router.push(router);
+ }
+ };
+
+ React.useEffect(effectApplicationStatusChange, [
+ application.status,
+ router,
+ stepState.activeStepIndex,
+ ]);
+ React.useEffect(effectReviewStepChange, [data, stepperDispatch]);
+ React.useEffect(effectSaveAndClose, [
+ data,
+ router,
+ stepState.activeStepIndex,
+ isSavingAndClosing,
+ ]);
+ React.useEffect(() => {
+ setIsSavingAndClosing(false);
+ }, [isError]);
+
+ const validateNextStep = (currentStepIndex: number): boolean => {
+ if (application.status === APPLICATION_STATUSES.INFO_REQUIRED) {
+ focusAndScroll('header-info-needed');
+ showErrorToast(
+ t('common:status.additional_information_needed'),
+ t(`common:applications.statuses.additionalInformationNeeded`)
+ );
+ return true;
+ }
+ const missing = {
+ status: !handledApplication?.status,
+ calculation:
+ (application.calculation.rows.length === 0 &&
+ handledApplication?.status === APPLICATION_STATUSES.ACCEPTED) ||
+ isRecalculationRequired ||
+ isCalculationsErrors,
+ logEntry:
+ handledApplication?.logEntryComment?.length <= 0 &&
+ handledApplication?.status === APPLICATION_STATUSES.REJECTED,
+ handler: false,
+ // Use longer length to take HTML tags into account
+ decisionText: handledApplication?.decisionText?.length <= 10,
+ justificationText: handledApplication?.justificationText?.length <= 10,
+ };
+
+ const errorStep1 =
+ missing.status || missing.calculation || missing.logEntry;
+
+ let errorStep2 = false;
+ if (currentStepIndex > 0) {
+ missing.handler = !['handler', 'manager'].includes(
+ handledApplication?.handlerRole
+ );
+
+ errorStep2 =
+ missing.decisionText || missing.justificationText || missing.handler;
+ }
+
+ if (errorStep1 || errorStep2) {
+ const missingFields = Object.keys(missing).filter((key) => missing[key]);
+ let interval = 0;
+ missingFields.forEach((key) => {
+ setTimeout(() => {
+ showErrorToast(
+ t('common:review.decisionProposal.errors.title'),
+ t(`common:review.decisionProposal.errors.fields.${key}`)
+ );
+ }, interval);
+ interval += 200;
+ });
+ }
+
+ return errorStep1 || errorStep2;
+ };
+
+ const handleNext = (finishProposal = false): void => {
+ if (finishProposal || stepState.activeStepIndex < 2) {
+ updateApplication({
+ ...handledApplication,
+ reviewStep: Math.min(stepState.activeStepIndex + 2, 4),
+ applicationId: application.id,
+ });
+ } else {
+ // Final step, just open confirmation modal
+ onDoneConfirmation();
+ }
+ };
+
+ const handlePrev = (): void => {
+ updateApplication({
+ ...handledApplication,
+ reviewStep: Math.max(0, stepState.activeStepIndex),
+ applicationId: application.id,
+ });
+ };
+
+ const handleSaveAndClose = (): void => {
+ updateApplication({
+ ...handledApplication,
+ reviewStep: stepState.activeStepIndex + 1,
+ applicationId: application.id,
+ });
+ setIsSavingAndClosing(true);
+ };
+
+ const handleClose = (): void => void router.push('/');
+
+ return (
+ <$Wrapper data-testid={dataTestId}>
+ <$Column>
+
+ {t(`${translationsBase}.close`)}
+
+ {application.status === APPLICATION_STATUSES.HANDLING && (
+
+ {t(`${translationsBase}.saveAndContinue`)}
+
+ )}
+
+ }
+ >
+ {t(`${translationsBase}.handlingPanel`)}
+
+
+ {![
+ APPLICATION_STATUSES.CANCELLED,
+ APPLICATION_STATUSES.ACCEPTED,
+ APPLICATION_STATUSES.REJECTED,
+ ].includes(application.status) &&
+ !application.batch &&
+ !application.archived && (
+ }
+ >
+ {t(`${translationsBase}.cancel`)}
+
+ )}
+ $Column>
+
+ {application?.status === APPLICATION_STATUSES.HANDLING && (
+ <$Column>
+ {stepState.activeStepIndex !== 0 && (
+ }
+ onClick={() => handlePrev()}
+ >
+ {t('common:utility.previous')}
+
+ )}
+
+ !validateNextStep(stepState.activeStepIndex) ? handleNext() : null
+ }
+ iconRight={lastStep ? undefined : }
+ >
+ {lastStep ? t('common:utility.send') : t('common:utility.next')}
+
+ $Column>
+ )}
+
+ {isConfirmationModalOpen && (
+ }
+ submitButtonIcon={ }
+ variant="danger"
+ customContent={
+
+ }
+ />
+ )}
+ {isDoneConfirmationModalOpen && (
+ }
+ className=""
+ variant="primary"
+ theme={theme.components.modal.coat}
+ customContent={
+ handleNext(true)}
+ calculationRows={application.calculation?.rows}
+ />
+ }
+ />
+ )}
+ }
+ customItemsNotes={
+ <$CustomNotesActions>
+
+ {t('common:messenger.showToHanlderOnly')}
+ $CustomNotesActions>
+ }
+ />
+ $Wrapper>
+ );
+};
+
+export default HandlingApplicationActions;
diff --git a/frontend/benefit/handler/src/components/applicationReview/applicationProcessingView/AplicationProcessingView.tsx b/frontend/benefit/handler/src/components/applicationReview/applicationProcessingView/ApplicationProcessingView.tsx
similarity index 80%
rename from frontend/benefit/handler/src/components/applicationReview/applicationProcessingView/AplicationProcessingView.tsx
rename to frontend/benefit/handler/src/components/applicationReview/applicationProcessingView/ApplicationProcessingView.tsx
index 9c542ac7ba..7a27321c46 100644
--- a/frontend/benefit/handler/src/components/applicationReview/applicationProcessingView/AplicationProcessingView.tsx
+++ b/frontend/benefit/handler/src/components/applicationReview/applicationProcessingView/ApplicationProcessingView.tsx
@@ -1,5 +1,6 @@
import ReviewSection from 'benefit/handler/components/reviewSection/ReviewSection';
import AppContext from 'benefit/handler/context/AppContext';
+import { useDetermineAhjoMode } from 'benefit/handler/hooks/useDetermineAhjoMode';
import { Application } from 'benefit/handler/types/application';
import { extractCalculatorRows } from 'benefit/handler/utils/calculator';
import { APPLICATION_STATUSES } from 'benefit-shared/constants';
@@ -35,6 +36,8 @@ const ApplicationProcessingView: React.FC<{ data: Application }> = ({
const { handledApplication, setHandledApplication } =
React.useContext(AppContext);
+ const isNewAhjoMode = useDetermineAhjoMode();
+
const toggleGrantedAsDeMinimisAid = (): void => {
setHandledApplication({
...handledApplication,
@@ -42,6 +45,20 @@ const ApplicationProcessingView: React.FC<{ data: Application }> = ({
});
};
+ React.useEffect(() => {
+ if (!handledApplication && isNewAhjoMode) {
+ setHandledApplication({
+ status: data?.decisionProposalDraft?.status,
+ grantedAsDeMinimisAid:
+ !!data?.decisionProposalDraft?.grantedAsDeMinimisAid,
+ logEntryComment: data?.decisionProposalDraft?.logEntryComment,
+ handlerRole: data?.decisionProposalDraft?.handlerRole,
+ justificationText: data?.decisionProposalDraft?.justificationText,
+ decisionText: data?.decisionProposalDraft?.decisionText,
+ });
+ }
+ }, [data, handledApplication, setHandledApplication, isNewAhjoMode]);
+
const onCommentsChange = (
event: React.ChangeEvent
): void =>
@@ -60,7 +77,9 @@ const ApplicationProcessingView: React.FC<{ data: Application }> = ({
return (
<$GridCell $colSpan={11}>
- <$MainHeader>{t(`${translationsBase}.headings.heading10`)}$MainHeader>
+ <$MainHeader css={{ margin: 'var(--spacing-xs) 0 var(--spacing-m)' }}>
+ {t(`${translationsBase}.headings.heading10`)}
+ $MainHeader>
<$Grid>
<$GridCell $colSpan={11}>
<$RadioButtonContainer>
@@ -73,6 +92,7 @@ const ApplicationProcessingView: React.FC<{ data: Application }> = ({
onChange={(value) => {
if (value) {
setHandledApplication({
+ ...handledApplication,
logEntryComment: '',
status: APPLICATION_STATUSES.REJECTED,
grantedAsDeMinimisAid: false,
@@ -114,7 +134,7 @@ const ApplicationProcessingView: React.FC<{ data: Application }> = ({
>
)}
$GridCell>
- <$GridCell $colSpan={11}>
+ <$GridCell $colSpan={11} css={{ marginBottom: 'var(--spacing-m)' }}>
<$Grid>
<$GridCell $colSpan={11}>
<$RadioButtonContainer>
@@ -127,6 +147,7 @@ const ApplicationProcessingView: React.FC<{ data: Application }> = ({
onChange={(value) => {
if (value) {
setHandledApplication({
+ ...handledApplication,
logEntryComment: '',
status: APPLICATION_STATUSES.ACCEPTED,
});
@@ -184,23 +205,25 @@ const ApplicationProcessingView: React.FC<{ data: Application }> = ({
))}
$GridCell>
)}
- <$GridCell
- $colSpan={8}
- $colStart={1}
- style={{ paddingTop: theme.spacing.l }}
- >
-
+ {!isNewAhjoMode && (
+ <$GridCell
+ $colSpan={8}
+ $colStart={1}
+ style={{ paddingTop: theme.spacing.l }}
+ >
+
- <$HelpText>
- {t(`${translationsBase}.actions.reasonAcceptPlaceholder`)}
- $HelpText>
- $GridCell>
+ <$HelpText>
+ {t(`${translationsBase}.actions.reasonAcceptPlaceholder`)}
+ $HelpText>
+ $GridCell>
+ )}
$Grid>
<$Grid>
<$GridCell $colSpan={12}>
diff --git a/frontend/benefit/handler/src/components/applicationReview/coOperationNegotiationsView/CoOperationNegotiationsView.tsx b/frontend/benefit/handler/src/components/applicationReview/coOperationNegotiationsView/CoOperationNegotiationsView.tsx
index 5177f9696b..7412656da5 100644
--- a/frontend/benefit/handler/src/components/applicationReview/coOperationNegotiationsView/CoOperationNegotiationsView.tsx
+++ b/frontend/benefit/handler/src/components/applicationReview/coOperationNegotiationsView/CoOperationNegotiationsView.tsx
@@ -25,7 +25,7 @@ const CoOperationNegotiationsView: React.FC = ({
<$ViewFieldBold>
{t(`${translationsBase}.fields.coOperationNegotiations`)}
$ViewFieldBold>
- <$ViewField>
+ <$ViewField large>
{` ${t(
`common:utility.${data.coOperationNegotiations ? 'yes' : 'no'}`
)}`}
@@ -37,7 +37,9 @@ const CoOperationNegotiationsView: React.FC = ({
`${translationsBase}.fields.coOperationNegotiationsDescription`
)}
$ViewFieldBold>
- <$ViewField>{data.coOperationNegotiationsDescription}$ViewField>
+ <$ViewField large>
+ {data.coOperationNegotiationsDescription}
+ $ViewField>
>
)}
$GridCell>
diff --git a/frontend/benefit/handler/src/components/applicationReview/companyInfoView/CompanyInfoView.tsx b/frontend/benefit/handler/src/components/applicationReview/companyInfoView/CompanyInfoView.tsx
index 46986270cf..7fc362ad8c 100644
--- a/frontend/benefit/handler/src/components/applicationReview/companyInfoView/CompanyInfoView.tsx
+++ b/frontend/benefit/handler/src/components/applicationReview/companyInfoView/CompanyInfoView.tsx
@@ -28,13 +28,13 @@ const CompanyInfoView: React.FC = ({ data }) => {
<$ViewFieldBold>
{t(`${translationsBase}.fields.businessId`)}
$ViewFieldBold>
- <$ViewField>{data.company?.businessId}$ViewField>
+ <$ViewField large>{data.company?.businessId}$ViewField>
$GridCell>
<$GridCell $colSpan={6}>
<$ViewFieldBold>
{t(`${translationsBase}.fields.organizationType`)}
$ViewFieldBold>
- <$ViewField>
+ <$ViewField large>
{t(`common:organizationTypes.${data.company?.organizationType}`)}
$ViewField>
$GridCell>
@@ -42,7 +42,7 @@ const CompanyInfoView: React.FC = ({ data }) => {
<$ViewFieldBold>
{t(`${translationsBase}.fields.address`)}
$ViewFieldBold>
- <$ViewField>{`${data.company?.streetAddress}, ${
+ <$ViewField large>{`${data.company?.streetAddress}, ${
data.company?.postcode || ''
} ${data.company?.city || ''}`}$ViewField>
$GridCell>
@@ -51,7 +51,7 @@ const CompanyInfoView: React.FC = ({ data }) => {
<$ViewFieldBold>
{t(`${translationsBase}.fields.alternativeAddress`)}
$ViewFieldBold>
- <$ViewField>
+ <$ViewField large>
{data.companyDepartment && {data.companyDepartment}
}
{[
data.alternativeCompanyStreetAddress,
@@ -67,7 +67,7 @@ const CompanyInfoView: React.FC = ({ data }) => {
<$ViewFieldBold>
{t(`${translationsBase}.fields.bankAccountNumber`)}
$ViewFieldBold>
- <$ViewField>
+ <$ViewField large>
{friendlyFormatIBAN(data?.companyBankAccountNumber)}
$ViewField>
$GridCell>
@@ -78,7 +78,7 @@ const CompanyInfoView: React.FC = ({ data }) => {
`${translationsBase}.fields.associationHasBusinessActivities.label`
)}
$ViewFieldBold>
- <$ViewField>
+ <$ViewField large>
{data?.associationHasBusinessActivities
? t(
`${translationsBase}.fields.associationHasBusinessActivities.yes`
diff --git a/frontend/benefit/handler/src/components/applicationReview/contactPersonView/ContactPersonView.tsx b/frontend/benefit/handler/src/components/applicationReview/contactPersonView/ContactPersonView.tsx
index 5434831714..984af206c2 100644
--- a/frontend/benefit/handler/src/components/applicationReview/contactPersonView/ContactPersonView.tsx
+++ b/frontend/benefit/handler/src/components/applicationReview/contactPersonView/ContactPersonView.tsx
@@ -30,17 +30,17 @@ const ContactPersonView: React.FC = ({ data }) => {
$GridCell>
<$GridCell $colSpan={6} $colStart={1}>
<$ViewFieldBold>{t(`${translationsBase}.fields.phone`)}$ViewFieldBold>
- <$ViewField>{data.companyContactPersonPhoneNumber}$ViewField>
+ <$ViewField large>{data.companyContactPersonPhoneNumber}$ViewField>
$GridCell>
<$GridCell $colSpan={6}>
<$ViewFieldBold>{t(`${translationsBase}.fields.email`)}$ViewFieldBold>
- <$ViewField>{data.companyContactPersonEmail}$ViewField>
+ <$ViewField large>{data.companyContactPersonEmail}$ViewField>
$GridCell>
<$GridCell $colSpan={6} $colStart={1}>
<$ViewFieldBold>
{t(`${translationsBase}.fields.applicantLanguage`)}
$ViewFieldBold>
- <$ViewField>
+ <$ViewField large>
{t(`common:languages.${data.applicantLanguage || ''}`)}
$ViewField>
$GridCell>
diff --git a/frontend/benefit/handler/src/components/applicationReview/deminimisView/DeminimisView.tsx b/frontend/benefit/handler/src/components/applicationReview/deminimisView/DeminimisView.tsx
index 7cdbbe5133..4088941f31 100644
--- a/frontend/benefit/handler/src/components/applicationReview/deminimisView/DeminimisView.tsx
+++ b/frontend/benefit/handler/src/components/applicationReview/deminimisView/DeminimisView.tsx
@@ -26,14 +26,14 @@ const DeminimisView: React.FC = ({ data }) => {
>
{data.deMinimisAidSet && data.deMinimisAidSet?.length > 0 ? (
<>
- <$GridCell $colSpan={4}>
+ <$GridCell $colSpan={3}>
<$SummaryTableHeader>
<$ViewFieldBold>
{t(`${translationsBase}.fields.deMinimisAidGranter`)}
$ViewFieldBold>
$SummaryTableHeader>
$GridCell>
- <$GridCell $colSpan={3}>
+ <$GridCell $colSpan={2}>
<$SummaryTableHeader>
<$ViewFieldBold>
{t(`${translationsBase}.fields.deMinimisAidAmount`)}
@@ -51,10 +51,10 @@ const DeminimisView: React.FC = ({ data }) => {
- <$GridCell $colStart={1} $colSpan={4}>
+ <$GridCell $colStart={1} $colSpan={3}>
<$SummaryTableValue>{granter}$SummaryTableValue>
$GridCell>
- <$GridCell $colSpan={3}>
+ <$GridCell $colSpan={2}>
<$SummaryTableValue>
{amount
? formatFloatToCurrency(amount, 'EUR', 'FI-fi', 0)
@@ -68,12 +68,12 @@ const DeminimisView: React.FC = ({ data }) => {
$GridCell>
))}
- <$GridCell $colSpan={4} $colStart={1}>
+ <$GridCell $colSpan={3} $colStart={1}>
<$SummaryTableLastLine>
{t(`${translationsBase}.fields.deMinimisAidTotal`)}
$SummaryTableLastLine>
$GridCell>
- <$GridCell $colSpan={3}>
+ <$GridCell $colSpan={2}>
<$SummaryTableLastLine>
{data?.totalDeminimisAmount
? formatFloatToCurrency(
diff --git a/frontend/benefit/handler/src/components/applicationReview/employeeView/EmployeeView.tsx b/frontend/benefit/handler/src/components/applicationReview/employeeView/EmployeeView.tsx
index 27537dd4d2..4a63766a2c 100644
--- a/frontend/benefit/handler/src/components/applicationReview/employeeView/EmployeeView.tsx
+++ b/frontend/benefit/handler/src/components/applicationReview/employeeView/EmployeeView.tsx
@@ -30,13 +30,13 @@ const EmployeeView: React.FC = ({ data }) => {
$GridCell>
<$GridCell $colSpan={6} $colStart={1}>
<$ViewFieldBold>{t(`${translationsBase}.fields.ssn`)}$ViewFieldBold>
- <$ViewField>{data.employee?.socialSecurityNumber}$ViewField>
+ <$ViewField large>{data.employee?.socialSecurityNumber}$ViewField>
$GridCell>
<$GridCell $colSpan={6} $colStart={1}>
<$ViewFieldBold>
{t(`${translationsBase}.fields.isLivingInHelsinki`)}
$ViewFieldBold>
- <$ViewField>
+ <$ViewField large>
{t(
`common:utility.${data.employee?.isLivingInHelsinki ? 'yes' : 'no'}`
)}
@@ -47,7 +47,7 @@ const EmployeeView: React.FC = ({ data }) => {
<$ViewFieldBold>
{t(`${translationsBase}.fields.associationImmediateManagerCheck`)}
$ViewFieldBold>
- <$ViewField>
+ <$ViewField large>
{t(
`common:utility.${
data.associationImmediateManagerCheck ? 'yes' : 'no'
diff --git a/frontend/benefit/handler/src/components/applicationReview/employmentView/EmpoymentView.tsx b/frontend/benefit/handler/src/components/applicationReview/employmentView/EmpoymentView.tsx
index 1411d11ba8..640e057fae 100644
--- a/frontend/benefit/handler/src/components/applicationReview/employmentView/EmpoymentView.tsx
+++ b/frontend/benefit/handler/src/components/applicationReview/employmentView/EmpoymentView.tsx
@@ -29,13 +29,13 @@ const EmploymentView: React.FC = ({ data }) => {
<$ViewFieldBold>
{t(`${translationsBase}.fields.jobTitle`)}
$ViewFieldBold>
- <$ViewField>{data.employee?.jobTitle || '-'}$ViewField>
+ <$ViewField large>{data.employee?.jobTitle || '-'}$ViewField>
$GridCell>
<$GridCell $colSpan={6}>
<$ViewFieldBold>
{t(`${translationsBase}.fields.workingHours`)}
$ViewFieldBold>
- <$ViewField>
+ <$ViewField large>
{parseFloat(data.employee?.workingHours).toLocaleString('fi-FI')}{' '}
{t(`${translationsBase}.fields.workingHoursText`)}
$ViewField>
@@ -44,7 +44,7 @@ const EmploymentView: React.FC = ({ data }) => {
<$ViewFieldBold>
{t(`${translationsBase}.fields.monthlyPay`)}
$ViewFieldBold>
- <$ViewField>
+ <$ViewField large>
{data.employee?.monthlyPay &&
t(`${translationsBase}.fields.monthlyPayText`, {
monthlyPay: formatFloatToCurrency(
@@ -60,7 +60,7 @@ const EmploymentView: React.FC = ({ data }) => {
<$ViewFieldBold>
{t(`${translationsBase}.fields.vacationMoney`)}
$ViewFieldBold>
- <$ViewField>
+ <$ViewField large>
{data.employee?.vacationMoney &&
t(`${translationsBase}.fields.vacationMoneyText`, {
vacationMoney: formatFloatToCurrency(
@@ -76,7 +76,7 @@ const EmploymentView: React.FC = ({ data }) => {
<$ViewFieldBold>
{t(`${translationsBase}.fields.otherExpenses`)}
$ViewFieldBold>
- <$ViewField>
+ <$ViewField large>
{data.employee?.otherExpenses &&
t(`${translationsBase}.fields.otherExpensesText`, {
otherExpenses: formatFloatToCurrency(
@@ -92,13 +92,15 @@ const EmploymentView: React.FC = ({ data }) => {
<$ViewFieldBold>
{t(`${translationsBase}.fields.collectiveBargainingAgreement`)}
$ViewFieldBold>
- <$ViewField>{data.employee?.collectiveBargainingAgreement}$ViewField>
+ <$ViewField large>
+ {data.employee?.collectiveBargainingAgreement}
+ $ViewField>
$GridCell>
<$GridCell $colSpan={6} $colStart={1}>
<$ViewFieldBold>
{t(`${translationsBase}.fields.paySubsidyGranted.label`)}
$ViewFieldBold>
- <$ViewField>
+ <$ViewField large>
{t(
`${translationsBase}.fields.paySubsidyGranted.${camelCase(
data.paySubsidyGranted
@@ -110,7 +112,7 @@ const EmploymentView: React.FC = ({ data }) => {
<$ViewFieldBold>
{t(`${translationsBase}.fields.apprenticeshipProgram`)}{' '}
$ViewFieldBold>
- <$ViewField>
+ <$ViewField large>
{t(`common:utility.${data.apprenticeshipProgram ? 'yes' : 'no'}`)}
$ViewField>
$GridCell>
diff --git a/frontend/benefit/handler/src/components/applicationReview/handlingView/ApplicationStepper.sc.ts b/frontend/benefit/handler/src/components/applicationReview/handlingView/ApplicationStepper.sc.ts
new file mode 100644
index 0000000000..fd4b36c9ec
--- /dev/null
+++ b/frontend/benefit/handler/src/components/applicationReview/handlingView/ApplicationStepper.sc.ts
@@ -0,0 +1,13 @@
+import styled from 'styled-components';
+
+export const $ApplicationStepperWrapper = styled.div`
+ h1 {
+ margin: 0 0 ${(props) => props.theme.spacing.m}
+ ${(props) => props.theme.spacing.xs2};
+ }
+
+ hr {
+ border: 1px solid ${(props) => props.theme.colors.silver};
+ margin-top: ${(props) => props.theme.spacing.m};
+ }
+`;
diff --git a/frontend/benefit/handler/src/components/applicationReview/handlingView/EditorAhjoProposal.sc.ts b/frontend/benefit/handler/src/components/applicationReview/handlingView/EditorAhjoProposal.sc.ts
new file mode 100644
index 0000000000..e02a44fb57
--- /dev/null
+++ b/frontend/benefit/handler/src/components/applicationReview/handlingView/EditorAhjoProposal.sc.ts
@@ -0,0 +1,48 @@
+import styled from 'styled-components';
+
+export const $EditorWrapper = styled.div`
+ position: relative;
+ overflow: scroll;
+ background: #fff;
+ border-radius: 7px;
+ border: 1px solid ${(props) => props.theme.colors.silverDark};
+`;
+
+const EDITOR_STYLES = `
+ p {
+ margin-top: 0;
+ line-height: 1.5;
+ font-size: 18px;
+ }
+
+ h2 {
+ margin-top: 0;
+ font-size: 20px;
+ line-height: 1.5;
+ margin-bottom: 10px;
+ }
+
+ p + h2 {
+ margin-top: 30px;
+ }
+`;
+
+export const $Toolbar = styled.header`
+ padding: 0 0.5em;
+ border-bottom: 1px solid ${(props) => props.theme.colors.silverDark};
+`;
+
+export const $Content = styled.div`
+ position: relative;
+ height: 100%;
+ .tiptap {
+ padding: 1.75em 1em 1em;
+ border-radius: 0;
+
+ ${EDITOR_STYLES}
+ }
+`;
+
+export const $EditorPreview = styled.div`
+ ${EDITOR_STYLES}
+`;
diff --git a/frontend/benefit/handler/src/components/applicationReview/handlingView/EditorAhjoProposal.tsx b/frontend/benefit/handler/src/components/applicationReview/handlingView/EditorAhjoProposal.tsx
new file mode 100644
index 0000000000..4dbe717276
--- /dev/null
+++ b/frontend/benefit/handler/src/components/applicationReview/handlingView/EditorAhjoProposal.tsx
@@ -0,0 +1,105 @@
+import Document from '@tiptap/extension-document';
+import Heading from '@tiptap/extension-heading';
+import History from '@tiptap/extension-history';
+import Paragraph from '@tiptap/extension-paragraph';
+import Text from '@tiptap/extension-text';
+import { EditorContent, useEditor } from '@tiptap/react';
+import AppContext from 'benefit/handler/context/AppContext';
+import { Button, IconArrowRedo, IconArrowUndo, IconTextTool } from 'hds-react';
+import React, { useEffect } from 'react';
+
+import { $Content, $EditorWrapper, $Toolbar } from './EditorAhjoProposal.sc';
+
+type EditorProps = {
+ resetWithContent?: string;
+ name: 'decisionText' | 'justificationText';
+};
+
+const EditorAhjoProposal: React.FC = ({
+ resetWithContent = '',
+ name,
+}: EditorProps) => {
+ const editor = useEditor({
+ extensions: [
+ Document,
+ History,
+ Paragraph,
+ Text,
+ Heading.configure({
+ levels: [2],
+ }),
+ ],
+ content: resetWithContent,
+ });
+
+ const { handledApplication, setHandledApplication } =
+ React.useContext(AppContext);
+
+ useEffect(() => {
+ if (editor && resetWithContent) {
+ editor.commands.setContent(resetWithContent);
+ }
+ }, [editor, resetWithContent]);
+
+ if (!editor) {
+ return null;
+ }
+
+ // TODO: perf, this is done too many times
+ editor.on('update', () => {
+ setHandledApplication({
+ ...handledApplication,
+ [name]: editor.getHTML(),
+ });
+ });
+
+ return (
+ <$EditorWrapper>
+ <$Toolbar>
+
+ editor.chain().focus().toggleHeading({ level: 2 }).run()
+ }
+ className={
+ editor.isActive('heading', { level: 2 }) ? 'is-active' : ''
+ }
+ >
+
+
+
+ editor.chain().focus().undo().run()}
+ disabled={!editor.can().undo()}
+ >
+
+
+ editor.chain().focus().redo().run()}
+ disabled={!editor.can().redo()}
+ >
+
+
+ $Toolbar>
+
+ <$Content>
+
+
+
+ $Content>
+ $EditorWrapper>
+ );
+};
+
+export default EditorAhjoProposal;
diff --git a/frontend/benefit/handler/src/components/applicationReview/handlingView/HandlingStep.sc.ts b/frontend/benefit/handler/src/components/applicationReview/handlingView/HandlingStep.sc.ts
new file mode 100644
index 0000000000..c5db6ee67a
--- /dev/null
+++ b/frontend/benefit/handler/src/components/applicationReview/handlingView/HandlingStep.sc.ts
@@ -0,0 +1,14 @@
+import { $Grid } from 'shared/components/forms/section/FormSection.sc';
+import styled from 'styled-components';
+
+type ReviewGridProps = {
+ border?: string;
+};
+
+export const $ReviewGrid = styled($Grid)`
+ box-sizing: border-box;
+ margin-bottom: ${(props) => props.theme.spacing.m};
+ gap: 0;
+ padding: ${(props) => props.theme.spacing.l};
+ border: ${(props) => (props.border ? `2px solid ${props.border}` : '0')};
+`;
diff --git a/frontend/benefit/handler/src/components/applicationReview/handlingView/HandlingStep1.tsx b/frontend/benefit/handler/src/components/applicationReview/handlingView/HandlingStep1.tsx
new file mode 100644
index 0000000000..8b266215df
--- /dev/null
+++ b/frontend/benefit/handler/src/components/applicationReview/handlingView/HandlingStep1.tsx
@@ -0,0 +1,98 @@
+import { HANDLED_STATUSES } from 'benefit/handler/constants';
+import ReviewStateContext from 'benefit/handler/context/ReviewStateContext';
+import {
+ APPLICATION_ORIGINS,
+ APPLICATION_STATUSES,
+} from 'benefit-shared/constants';
+import { Application } from 'benefit-shared/types/application';
+import { ErrorData } from 'benefit-shared/types/common';
+import * as React from 'react';
+import Container from 'shared/components/container/Container';
+
+import ApplicationProcessingView from '../applicationProcessingView/ApplicationProcessingView';
+import BenefitView from '../benefitView/BenefitView';
+import CompanyInfoView from '../companyInfoView/CompanyInfoView';
+import ConsentView from '../consentView/ConsentView';
+import ContactPersonView from '../contactPersonView/ContactPersonView';
+import CoOperationNegotiationsView from '../coOperationNegotiationsView/CoOperationNegotiationsView';
+import DeminimisView from '../deminimisView/DeminimisView';
+import EmployeeView from '../employeeView/EmployeeView';
+import EmploymentView from '../employmentView/EmpoymentView';
+import ArchivedView from '../handledView/archivedView/ArchivedView';
+import HandledView from '../handledView/HandledView';
+import PaperView from '../paperView/PaperView';
+import SalaryBenefitCalculatorView from '../salaryBenefitCalculatorView/SalaryBenefitCalculatorView';
+import { useApplicationReview } from '../useApplicationReview';
+
+type HandlingStepProps = {
+ application: Application;
+ setIsRecalculationRequired: React.Dispatch>;
+ isRecalculationRequired: boolean;
+ calculationsErrors: ErrorData | undefined | null;
+ setCalculationErrors: React.Dispatch>;
+};
+
+const HandlingStep1: React.FC = ({
+ application,
+ setIsRecalculationRequired,
+ isRecalculationRequired,
+ calculationsErrors,
+ setCalculationErrors,
+}) => {
+ const { isUploading, handleUpload, reviewState, handleUpdateReviewState } =
+ useApplicationReview();
+
+ return (
+
+
+ {application.applicationOrigin === APPLICATION_ORIGINS.HANDLER && (
+
+ )}
+
+
+
+
+
+
+
+
+ {application.status === APPLICATION_STATUSES.HANDLING && (
+ <>
+
+
+ >
+ )}
+ {application.status &&
+ HANDLED_STATUSES.includes(application.status) && (
+
+ )}
+ {application.archived && }
+
+
+ );
+};
+
+export default HandlingStep1;
diff --git a/frontend/benefit/handler/src/components/applicationReview/handlingView/HandlingStep2.tsx b/frontend/benefit/handler/src/components/applicationReview/handlingView/HandlingStep2.tsx
new file mode 100644
index 0000000000..b0f396d8ef
--- /dev/null
+++ b/frontend/benefit/handler/src/components/applicationReview/handlingView/HandlingStep2.tsx
@@ -0,0 +1,277 @@
+import AppContext from 'benefit/handler/context/AppContext';
+import { DecisionProposalTemplateData } from 'benefit/handler/types/common';
+import { DECISION_TYPES } from 'benefit-shared/constants';
+import { Application } from 'benefit-shared/types/application';
+import { Select, SelectionGroup } from 'hds-react';
+import { useTranslation } from 'next-i18next';
+import * as React from 'react';
+import Container from 'shared/components/container/Container';
+import { $RadioButton } from 'shared/components/forms/fields/Fields.sc';
+import Heading from 'shared/components/forms/heading/Heading';
+import { $GridCell } from 'shared/components/forms/section/FormSection.sc';
+import theme from 'shared/styles/theme';
+
+import useDecisionProposalTemplateQuery from '../../../hooks/applicationHandling/useDecisionProposalTemplateQuery';
+import CalculationReview from '../CalculationReview';
+import EditorAhjoProposal from './EditorAhjoProposal';
+import { $ReviewGrid } from './HandlingStep.sc';
+
+type HandlingStepProps = {
+ application: Application;
+};
+
+const ApplicationReviewStep2: React.FC = ({
+ application,
+}) => {
+ const { applicantLanguage, id } = application;
+
+ const { t } = useTranslation();
+ const translationBase = 'common:review.decisionProposal';
+ const { handledApplication, setHandledApplication } =
+ React.useContext(AppContext);
+
+ const [templateForJustificationText, setTemplateForJustificationText] =
+ React.useState(handledApplication?.justificationText || '');
+ const [templateForDecisionText, setTemplateForDecisionText] =
+ React.useState(handledApplication?.decisionText || '');
+
+ const decisionType =
+ handledApplication?.status === 'accepted'
+ ? DECISION_TYPES.ACCEPTED
+ : DECISION_TYPES.DENIED;
+
+ const { data: sections } = useDecisionProposalTemplateQuery(id, decisionType);
+
+ const replaceDecisionTemplatePlaceholders = (
+ text: string,
+ role: string
+ ): string => {
+ let replacedText = '';
+
+ const translations = {
+ handler: t(`${translationBase}.role.fields.decisionMaker.manager`),
+ manager: t(`${translationBase}.role.fields.decisionMaker.handler`),
+ currentRole: t(`${translationBase}.role.fields.decisionMaker.${role}`),
+ };
+
+ replacedText = text.replace(/@role/gi, translations.currentRole);
+ // eslint-disable-next-line security/detect-non-literal-regexp
+ const roleKeyRegExp = new RegExp(
+ `(${translations.handler}|${translations.manager})`,
+ 'gi'
+ );
+
+ replacedText = replacedText.replace(
+ roleKeyRegExp,
+ translations.currentRole
+ );
+
+ return replacedText;
+ };
+ const selectTemplate = (option: DecisionProposalTemplateData): void => {
+ setTemplateForDecisionText(
+ replaceDecisionTemplatePlaceholders(
+ option.template_decision_text,
+ handledApplication?.handlerRole
+ )
+ );
+ setTemplateForJustificationText(
+ replaceDecisionTemplatePlaceholders(
+ option.template_justification_text,
+ handledApplication?.handlerRole
+ )
+ );
+
+ setHandledApplication({
+ ...handledApplication,
+ decisionText: replaceDecisionTemplatePlaceholders(
+ option.template_decision_text,
+ handledApplication?.handlerRole
+ ),
+ justificationText: replaceDecisionTemplatePlaceholders(
+ option.template_justification_text,
+ handledApplication?.handlerRole
+ ),
+ });
+ };
+
+ if (!sections || sections?.length === 0) {
+ const language =
+ applicantLanguage === 'en' ? 'fi' : (applicantLanguage as 'fi' | 'sv');
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ <$ReviewGrid bgColor={theme.colors.silverLight}>
+
+ $ReviewGrid>
+
+ <$ReviewGrid bgColor={theme.colors.silverLight}>
+ <$GridCell $colSpan={12}>
+
+ $GridCell>
+ <$GridCell $colSpan={12}>
+
+ <$RadioButton
+ checked={handledApplication?.handlerRole === 'handler'}
+ key="radio-decision-maker-handler"
+ id="radio-decision-maker-handler"
+ value="handler"
+ label={t(`${translationBase}.role.fields.decisionMaker.handler`)}
+ onChange={(value) => {
+ if (value) {
+ setTemplateForDecisionText(
+ replaceDecisionTemplatePlaceholders(
+ handledApplication?.decisionText || '',
+ 'handler'
+ )
+ );
+ setTemplateForJustificationText(
+ replaceDecisionTemplatePlaceholders(
+ handledApplication?.justificationText || '',
+ 'handler'
+ )
+ );
+ setHandledApplication({
+ ...handledApplication,
+ handlerRole: 'handler',
+ decisionText: replaceDecisionTemplatePlaceholders(
+ handledApplication?.decisionText || '',
+ 'handler'
+ ),
+ justificationText: replaceDecisionTemplatePlaceholders(
+ handledApplication?.justificationText || '',
+ 'handler'
+ ),
+ });
+ }
+ }}
+ />
+ <$RadioButton
+ checked={handledApplication?.handlerRole === 'manager'}
+ key="radio-decision-maker-manager"
+ id="radio-decision-maker-manager"
+ value="manager"
+ label={t(`${translationBase}.role.fields.decisionMaker.manager`)}
+ name="inspection_mode"
+ onChange={(value) => {
+ if (value) {
+ setTemplateForDecisionText(
+ replaceDecisionTemplatePlaceholders(
+ handledApplication?.decisionText || '',
+ 'manager'
+ )
+ );
+ setTemplateForJustificationText(
+ replaceDecisionTemplatePlaceholders(
+ handledApplication?.justificationText || '',
+ 'manager'
+ )
+ );
+ setHandledApplication({
+ ...handledApplication,
+ handlerRole: 'manager',
+ decisionText: replaceDecisionTemplatePlaceholders(
+ handledApplication?.decisionText || '',
+ 'manager'
+ ),
+ justificationText: replaceDecisionTemplatePlaceholders(
+ handledApplication?.justificationText || '',
+ 'manager'
+ ),
+ });
+ }
+ }}
+ />
+
+ $GridCell>
+ $ReviewGrid>
+ {handledApplication?.handlerRole && (
+ <$ReviewGrid bgColor={theme.colors.silverLight}>
+ <$GridCell $colSpan={12}>
+
+
+
+ {t(`${translationBase}.templates.fields.usedLanguage.label`)}{' '}
+
+
+ {t(`common:languages.${applicantLanguage}`).toLowerCase()}
+
+ .
+
+
+ $GridCell>
+ <$GridCell $colSpan={5} css={{ marginBottom: theme.spacing.m }}>
+ ({
+ ...section,
+ label: section.name,
+ })
+ )}
+ onChange={selectTemplate}
+ style={{ marginTop: theme.spacing.xs }}
+ />
+ $GridCell>
+
+ <$GridCell $colSpan={12}>
+
+
+ $GridCell>
+ <$GridCell $colSpan={12}>
+
+
+ $GridCell>
+ $ReviewGrid>
+ )}
+
+ );
+};
+
+export default ApplicationReviewStep2;
diff --git a/frontend/benefit/handler/src/components/applicationReview/handlingView/HandlingStep3.tsx b/frontend/benefit/handler/src/components/applicationReview/handlingView/HandlingStep3.tsx
new file mode 100644
index 0000000000..abfef545ea
--- /dev/null
+++ b/frontend/benefit/handler/src/components/applicationReview/handlingView/HandlingStep3.tsx
@@ -0,0 +1,101 @@
+import { Application } from 'benefit-shared/types/application';
+import { useTranslation } from 'next-i18next';
+import * as React from 'react';
+import Container from 'shared/components/container/Container';
+import Heading from 'shared/components/forms/heading/Heading';
+import { $GridCell } from 'shared/components/forms/section/FormSection.sc';
+import theme from 'shared/styles/theme';
+
+import CalculationReview from '../CalculationReview';
+import { $EditorPreview } from './EditorAhjoProposal.sc';
+import { $ReviewGrid } from './HandlingStep.sc';
+
+type HandlingStepProps = {
+ application: Application;
+};
+
+const ApplicationReviewStep3: React.FC = ({
+ application,
+}) => {
+ const { t } = useTranslation();
+ return (
+
+ <$ReviewGrid border={theme.colors.silver}>
+
+ $ReviewGrid>
+ <$ReviewGrid border={theme.colors.silver}>
+ <$GridCell $colSpan={12}>
+
+
+
+
+ {t('common:review.decisionProposal.preview.ahjoIdentifier')}
+
+
+
+ <$EditorPreview>
+
+ $EditorPreview>
+ $GridCell>
+
+ <$GridCell $colSpan={12}>
+
+ <$EditorPreview>
+
+ $EditorPreview>
+ $GridCell>
+ $ReviewGrid>
+
+ );
+};
+
+export default ApplicationReviewStep3;
diff --git a/frontend/benefit/handler/src/components/applicationReview/handlingView/HandlingStepper.tsx b/frontend/benefit/handler/src/components/applicationReview/handlingView/HandlingStepper.tsx
new file mode 100644
index 0000000000..402ac4acec
--- /dev/null
+++ b/frontend/benefit/handler/src/components/applicationReview/handlingView/HandlingStepper.tsx
@@ -0,0 +1,55 @@
+import { StepStateType } from 'benefit/handler/hooks/applicationHandling/useHandlingStepper';
+import { Stepper } from 'hds-react';
+import { useTranslation } from 'next-i18next';
+import * as React from 'react';
+import Container from 'shared/components/container/Container';
+import Heading from 'shared/components/forms/heading/Heading';
+import theme from 'shared/styles/theme';
+
+import { $ApplicationStepperWrapper } from './ApplicationStepper.sc';
+
+type ApplicationStepperProps = { stepState: StepStateType };
+
+const ApplicationStepper: React.FC = ({
+ stepState,
+}: ApplicationStepperProps) => {
+ const { t } = useTranslation();
+
+ return (
+
+ <$ApplicationStepperWrapper>
+
+ e.stopPropagation()}
+ />
+
+ {stepState.activeStepIndex === 2 && (
+
+ )}
+
+ $ApplicationStepperWrapper>
+
+ );
+};
+
+export default ApplicationStepper;
diff --git a/frontend/benefit/handler/src/components/applicationReview/notificationView/NotificationView.tsx b/frontend/benefit/handler/src/components/applicationReview/notificationView/NotificationView.tsx
index 4f5c9e928e..1bd78f4dad 100644
--- a/frontend/benefit/handler/src/components/applicationReview/notificationView/NotificationView.tsx
+++ b/frontend/benefit/handler/src/components/applicationReview/notificationView/NotificationView.tsx
@@ -1,8 +1,9 @@
import { ROUTES } from 'benefit/handler/constants';
import AppContext from 'benefit/handler/context/AppContext';
+import { useDetermineAhjoMode } from 'benefit/handler/hooks/useDetermineAhjoMode';
import { APPLICATION_STATUSES } from 'benefit-shared/constants';
import { Application } from 'benefit-shared/types/application';
-import { Button } from 'hds-react';
+import { Button, IconLinkExternal } from 'hds-react';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import * as React from 'react';
@@ -30,27 +31,27 @@ const NotificationView: React.FC = ({ data }) => {
const translationsBase = 'common:notifications';
const { t } = useTranslation();
const router = useRouter();
- const { handledApplication, setHandledApplication } =
- React.useContext(AppContext);
+ const { setHandledApplication } = React.useContext(AppContext);
useEffect(() => () => setHandledApplication(null), [setHandledApplication]);
const handleGoHome = (): void => {
void router.push(ROUTES.HOME);
};
-
const handleStartAhjo = (): void => {
- const activeTab =
- handledApplication?.status === APPLICATION_STATUSES.ACCEPTED ? 4 : 5;
+ const activeTab = data?.status === APPLICATION_STATUSES.ACCEPTED ? 4 : 5;
void router.push({ pathname: ROUTES.HOME, query: { tab: activeTab } });
};
+ const isNewAhjoMode = useDetermineAhjoMode();
+ const translationKey = data?.status ?? 'error';
+
return (
<$Notification>
<$Grid>
<$GridCell $colSpan={2}>
- {handledApplication?.status ? (
+ {translationKey !== 'error' ? (
<$IconCheckCircle size="xl" />
) : (
<$IconAlertCircle size="xl" />
@@ -58,28 +59,20 @@ const NotificationView: React.FC = ({ data }) => {
$GridCell>
<$GridCell $colSpan={10}>
<$NotificationTitle>
- {t(
- `${translationsBase}.${String(
- handledApplication?.status ?? 'error'
- )}.title`
- )}
+ {isNewAhjoMode
+ ? t(`common:review.decisionProposal.submitted.title`)
+ : t(`${translationsBase}.${translationKey}.title`)}
$NotificationTitle>
<$NotificationMessage>
- {t(
- `${translationsBase}.${String(
- handledApplication?.status ?? 'error'
- )}.message`,
- { applicationNum: data?.applicationNumber }
- )}
- $NotificationMessage>
- $GridCell>
- <$GridCell $colSpan={10} $colStart={3}>
- <$NotificationMessage>
- {t(`${translationsBase}.ahjoMessage.message`)}
+ {isNewAhjoMode
+ ? t(`common:review.decisionProposal.submitted.text`)
+ : t(`${translationsBase}.${translationKey}.message`, {
+ applicationNum: data?.applicationNumber,
+ })}
$NotificationMessage>
$GridCell>
- <$GridCell $colSpan={10} $colStart={3}>
- {handledApplication && (
+ {isNewAhjoMode && (
+ <$GridCell $colSpan={10} $colStart={3}>
<$ActionsContainer>
{t('common:utility.home')}
@@ -88,12 +81,41 @@ const NotificationView: React.FC = ({ data }) => {
variant="secondary"
theme="coat"
onClick={handleStartAhjo}
+ iconRight={ }
>
- {t(`${translationsBase}.ahjoButton.label`)}
+ {t(`${translationsBase}.ahjoButton.linkLabel`)}
$ActionsContainer>
- )}
- $GridCell>
+ $GridCell>
+ )}
+ {!isNewAhjoMode && (
+ <>
+ <$GridCell $colSpan={10} $colStart={3}>
+ <$NotificationMessage>
+ {t(`${translationsBase}.ahjoMessage.message`)}
+ $NotificationMessage>
+ $GridCell>
+ {[
+ APPLICATION_STATUSES.ACCEPTED,
+ APPLICATION_STATUSES.REJECTED,
+ ].includes(data?.status) && (
+ <$GridCell $colSpan={10} $colStart={3}>
+ <$ActionsContainer>
+
+ {t('common:utility.home')}
+
+
+ {t(`${translationsBase}.ahjoButton.label`)}
+
+ $ActionsContainer>
+ $GridCell>
+ )}
+ >
+ )}
$Grid>
$Notification>
diff --git a/frontend/benefit/handler/src/components/applicationReview/paperView/PaperView.tsx b/frontend/benefit/handler/src/components/applicationReview/paperView/PaperView.tsx
index 401e0676ac..d5d328c217 100644
--- a/frontend/benefit/handler/src/components/applicationReview/paperView/PaperView.tsx
+++ b/frontend/benefit/handler/src/components/applicationReview/paperView/PaperView.tsx
@@ -27,7 +27,7 @@ const PaperView: React.FC = ({ data }) => {
<$ViewFieldBold>
{t(`${translationsBase}.fields.paperApplicationDate`)}
$ViewFieldBold>
- <$ViewField>
+ <$ViewField large>
{convertToUIDateFormat(data?.paperApplicationDate) || '-'}
$ViewField>
$GridCell>
diff --git a/frontend/benefit/handler/src/components/applicationReview/salaryBenefitCalculatorView/SalaryBenefitCalculatorView.tsx b/frontend/benefit/handler/src/components/applicationReview/salaryBenefitCalculatorView/SalaryBenefitCalculatorView.tsx
index e661c18ddb..4040a50f05 100644
--- a/frontend/benefit/handler/src/components/applicationReview/salaryBenefitCalculatorView/SalaryBenefitCalculatorView.tsx
+++ b/frontend/benefit/handler/src/components/applicationReview/salaryBenefitCalculatorView/SalaryBenefitCalculatorView.tsx
@@ -40,11 +40,16 @@ import { useSalaryBenefitCalculatorData } from './useSalaryBenefitCalculatorData
const SalaryBenefitCalculatorView: React.FC<
SalaryBenefitCalculatorViewProps
-> = ({ data }) => {
+> = ({
+ application,
+ isRecalculationRequired,
+ setIsRecalculationRequired,
+ setCalculationErrors,
+ calculationsErrors,
+}) => {
const {
formik,
fields,
- calculationsErrors,
grantedPeriod,
stateAidMaxPercentageOptions,
getStateAidMaxPercentageSelectValue,
@@ -57,7 +62,7 @@ const SalaryBenefitCalculatorView: React.FC<
addNewTrainingCompensation,
removeTrainingCompensation,
isDisabledAddTrainingCompensationButton,
- } = useSalaryBenefitCalculatorData(data);
+ } = useSalaryBenefitCalculatorData(application, setCalculationErrors);
const {
t,
translationsBase,
@@ -66,9 +71,11 @@ const SalaryBenefitCalculatorView: React.FC<
getErrorMessage,
handleSubmit,
handleClear,
- isRecalculationRequired,
- setIsRecalculationRequired,
- } = useCalculatorData(CALCULATION_TYPES.SALARY, formik);
+ } = useCalculatorData(
+ CALCULATION_TYPES.SALARY,
+ formik,
+ setIsRecalculationRequired
+ );
const eurosPerMonth = 'common:utility.eurosPerMonth';
return (
@@ -189,7 +196,8 @@ const SalaryBenefitCalculatorView: React.FC<
<$ViewField>
{t(`${translationsBase}.paySubsidy`)}{' '}
- {data.paySubsidyGranted === PAY_SUBSIDY_GRANTED.GRANTED_AGED
+ {application.paySubsidyGranted ===
+ PAY_SUBSIDY_GRANTED.GRANTED_AGED
? `(${t(
'applications.sections.fields.paySubsidyGranted.grantedAged'
)})`
@@ -364,7 +372,7 @@ const SalaryBenefitCalculatorView: React.FC<
>
)}
- {!isManualCalculator && data.apprenticeshipProgram && (
+ {!isManualCalculator && application.apprenticeshipProgram && (
<>
<$GridCell $colStart={1} $colSpan={11}>
<$CalculatorHeader
@@ -546,15 +554,17 @@ const SalaryBenefitCalculatorView: React.FC<
$CalculatorHeader>
$GridCell>
- {data.startDate && data.endDate && (
+ {application.startDate && application.endDate && (
<$GridCell $colStart={1} $colSpan={11} style={{ alignSelf: 'center' }}>
<$ViewField style={{ marginBottom: theme.spacing.xs }}>
{t(`${translationsBase}.appliedPeriod`)}
{t(`${translationsBase}.startEndDates`, {
- startDate: convertToUIDateFormat(data.startDate),
- endDate: convertToUIDateFormat(data.endDate),
- period: formatStringFloatValue(data.durationInMonthsRounded),
+ startDate: convertToUIDateFormat(application.startDate),
+ endDate: convertToUIDateFormat(application.endDate),
+ period: formatStringFloatValue(
+ application.durationInMonthsRounded
+ ),
})}
$ViewField>
@@ -627,6 +637,7 @@ const SalaryBenefitCalculatorView: React.FC<
{t(`${translationsBase}.calculate`)}
@@ -641,7 +652,7 @@ const SalaryBenefitCalculatorView: React.FC<
$GridCell>
- {isRecalculationRequired && data?.calculation?.rows.length > 0 && (
+ {isRecalculationRequired && application?.calculation?.rows.length > 0 && (
<$GridCell $colStart={1} $colSpan={11}>
<$Notification
type="alert"
@@ -654,7 +665,7 @@ const SalaryBenefitCalculatorView: React.FC<
{!calculationsErrors && (
diff --git a/frontend/benefit/handler/src/components/applicationReview/salaryBenefitCalculatorView/SalaryCalculatorResults/SalaryCalculatorResults.tsx b/frontend/benefit/handler/src/components/applicationReview/salaryBenefitCalculatorView/SalaryCalculatorResults/SalaryCalculatorResults.tsx
index 4ff33120d7..13ec181053 100644
--- a/frontend/benefit/handler/src/components/applicationReview/salaryBenefitCalculatorView/SalaryCalculatorResults/SalaryCalculatorResults.tsx
+++ b/frontend/benefit/handler/src/components/applicationReview/salaryBenefitCalculatorView/SalaryCalculatorResults/SalaryCalculatorResults.tsx
@@ -52,7 +52,7 @@ const SalaryCalculatorResults: React.FC = ({
<$CalculatorTableHeader>
{t(`${translationsBase}.header`)}
$CalculatorTableHeader>
- <$Highlight>
+ <$Highlight data-testid="calculation-results-total">
{totalRowDescription
? totalRowDescription.descriptionFi
diff --git a/frontend/benefit/handler/src/components/applicationReview/salaryBenefitCalculatorView/useSalaryBenefitCalculatorData.ts b/frontend/benefit/handler/src/components/applicationReview/salaryBenefitCalculatorView/useSalaryBenefitCalculatorData.ts
index b1107004e8..49e917149e 100644
--- a/frontend/benefit/handler/src/components/applicationReview/salaryBenefitCalculatorView/useSalaryBenefitCalculatorData.ts
+++ b/frontend/benefit/handler/src/components/applicationReview/salaryBenefitCalculatorView/useSalaryBenefitCalculatorData.ts
@@ -31,7 +31,6 @@ type ExtendedComponentProps = {
fields: {
[key in CALCULATION_SALARY_KEYS]: Field
;
};
- calculationsErrors: ErrorData | undefined | null;
grantedPeriod: number;
stateAidMaxPercentageOptions: OptionType[];
getStateAidMaxPercentageSelectValue: () => OptionType | undefined;
@@ -56,7 +55,8 @@ const initialTrainingCompensationValues = {
};
const useSalaryBenefitCalculatorData = (
- application: Application
+ application: Application,
+ setCalculationErrors: React.Dispatch>
): ExtendedComponentProps => {
const { t } = useTranslation();
@@ -64,8 +64,10 @@ const useSalaryBenefitCalculatorData = (
!!application.calculation?.overrideMonthlyBenefitAmount
);
- const { calculateSalaryBenefit, calculationsErrors } =
- useHandlerReviewActions(application);
+ const { calculateSalaryBenefit } = useHandlerReviewActions(
+ application,
+ setCalculationErrors
+ );
const [newTrainingCompensation, setNewTrainingCompensation] =
useState(clone(initialTrainingCompensationValues));
@@ -221,7 +223,6 @@ const useSalaryBenefitCalculatorData = (
return {
formik,
fields,
- calculationsErrors,
grantedPeriod,
stateAidMaxPercentageOptions,
getStateAidMaxPercentageSelectValue,
diff --git a/frontend/benefit/handler/src/components/applicationReview/useApplicationReview.ts b/frontend/benefit/handler/src/components/applicationReview/useApplicationReview.ts
index c276ae64c9..8c90b8bdfd 100644
--- a/frontend/benefit/handler/src/components/applicationReview/useApplicationReview.ts
+++ b/frontend/benefit/handler/src/components/applicationReview/useApplicationReview.ts
@@ -38,7 +38,7 @@ const useApplicationReview = (): ExtendedComponentProps => {
const id = router?.query?.id?.toString() ?? '';
const [isLoading, setIsLoading] = useState(true);
- const shouldShowUpdatedToast = router?.query?.updated;
+ const shouldShowUpdatedToast = router?.query?.action === 'update';
const {
status: applicationDataStatus,
@@ -141,8 +141,14 @@ const useApplicationReview = (): ExtendedComponentProps => {
applicationNumber: application.applicationNumber,
})
);
+ window.history.replaceState(
+ null,
+ '',
+ `${window.location.pathname}?id=${application.id}`
+ );
}
- }, [shouldShowUpdatedToast, t, application.applicationNumber]);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
return {
t,
diff --git a/frontend/benefit/handler/src/components/batchProcessing/ApplicationsHandled.tsx b/frontend/benefit/handler/src/components/batchProcessing/ApplicationsHandled.tsx
index 0bb507dfe2..f62c6e2909 100644
--- a/frontend/benefit/handler/src/components/batchProcessing/ApplicationsHandled.tsx
+++ b/frontend/benefit/handler/src/components/batchProcessing/ApplicationsHandled.tsx
@@ -4,8 +4,9 @@ import {
} from 'benefit/handler/components/applicationList/ApplicationList.sc';
import useAddToBatchQuery from 'benefit/handler/hooks/useApplicationToBatch';
import { APPLICATION_STATUSES } from 'benefit-shared/constants';
-import { Button, LoadingSpinner, Table } from 'hds-react';
+import { Button, Table } from 'hds-react';
import React, { useEffect, useMemo, useState } from 'react';
+import LoadingSkeleton from 'react-loading-skeleton';
import Container from 'shared/components/container/Container';
import { $Link } from 'shared/components/table/Table.sc';
import theme from 'shared/styles/theme';
@@ -89,11 +90,16 @@ const ApplicationsHandled: React.FC = ({
if (shouldShowSkeleton) {
return (
- <$Heading>
+ <$Heading css={{ marginBottom: theme.spacing.xs }}>
{t(`common:applications.list.headings.${status}`)}{' '}
{t(`common:applications.list.headings.decisions`)}
$Heading>
-
+
+
);
}
diff --git a/frontend/benefit/handler/src/components/batchProcessing/BatchApplicationList.tsx b/frontend/benefit/handler/src/components/batchProcessing/BatchApplicationList.tsx
index 94b6a43a16..264f19b428 100644
--- a/frontend/benefit/handler/src/components/batchProcessing/BatchApplicationList.tsx
+++ b/frontend/benefit/handler/src/components/batchProcessing/BatchApplicationList.tsx
@@ -42,7 +42,7 @@ import styled from 'styled-components';
import { $Empty } from '../applicationList/ApplicationList.sc';
import ConfirmModalContent from '../applicationReview/actions/ConfirmModalContent/confirm';
import {
- $HorizontalList,
+ $HorizontalListTableHeader,
$TableBody,
$TableFooter,
$TableGrid,
@@ -246,7 +246,7 @@ const BatchApplicationList: React.FC = ({ batch }: BatchProps) => {
) : null
}
/>
- <$HorizontalList>
+ <$HorizontalListTableHeader>
{t('common:batches.single')}
{proposalForDecisionHeader()}
@@ -291,13 +291,14 @@ const BatchApplicationList: React.FC = ({ batch }: BatchProps) => {
{applications.length > 0 ? (
setIsCollapsed(!isCollapsed)}
>
{isCollapsed ? : }
) : null}
- $HorizontalList>
+ $HorizontalListTableHeader>
{applications?.length ? (
<$TableBody $isCollapsed={isCollapsed} aria-hidden={isCollapsed}>
diff --git a/frontend/benefit/handler/src/components/sidebar/Sidebar.tsx b/frontend/benefit/handler/src/components/sidebar/Sidebar.tsx
index dbd444ab02..0173c494f7 100644
--- a/frontend/benefit/handler/src/components/sidebar/Sidebar.tsx
+++ b/frontend/benefit/handler/src/components/sidebar/Sidebar.tsx
@@ -34,6 +34,7 @@ const Sidebar: React.FC = ({
isOpen={isOpen}
onClose={onClose}
closeText={t('common:messenger.close')}
+ stickyBarInUse
>
<$TabList>
diff --git a/frontend/benefit/handler/src/components/table/TableExtras.sc.ts b/frontend/benefit/handler/src/components/table/TableExtras.sc.ts
index 16e29b4989..8f38111e52 100644
--- a/frontend/benefit/handler/src/components/table/TableExtras.sc.ts
+++ b/frontend/benefit/handler/src/components/table/TableExtras.sc.ts
@@ -16,15 +16,9 @@ export const $HorizontalList = styled.dl`
margin: 0;
div {
- min-width: 200px;
+ min-width: 100px;
max-width: 300px;
margin-right: auto;
- &:last-child {
- margin-left: auto;
- margin-right: var(--spacing-m);
- min-width: var(--spacing-l);
- max-width: var(--spacing-l);
- }
}
dt,
@@ -49,6 +43,15 @@ export const $HorizontalList = styled.dl`
}
`;
+export const $HorizontalListTableHeader = styled($HorizontalList)`
+ div:last-child {
+ margin-left: auto;
+ margin-right: var(--spacing-m);
+ min-width: var(--spacing-l);
+ max-width: var(--spacing-l);
+ }
+`;
+
type TableGridProps = {
animateClose: boolean;
};
diff --git a/frontend/benefit/handler/src/hooks/applicationHandling/useDecisionProposalDraftMutation.tsx b/frontend/benefit/handler/src/hooks/applicationHandling/useDecisionProposalDraftMutation.tsx
new file mode 100644
index 0000000000..b5003a9a1a
--- /dev/null
+++ b/frontend/benefit/handler/src/hooks/applicationHandling/useDecisionProposalDraftMutation.tsx
@@ -0,0 +1,67 @@
+import { AxiosError } from 'axios';
+import { Application } from 'benefit/handler/types/application';
+import { BackendEndpoint } from 'benefit-shared/backend-api/backend-api';
+import {
+ DecisionProposalDraft,
+ DecisionProposalDraftData,
+} from 'benefit-shared/types/application';
+import { prettyPrintObject } from 'benefit-shared/utils/errors';
+import camelcaseKeys from 'camelcase-keys';
+import { useTranslation } from 'next-i18next';
+import React from 'react';
+import { useMutation, UseMutationResult, useQueryClient } from 'react-query';
+import hdsToast from 'shared/components/toast/Toast';
+import useBackendAPI from 'shared/hooks/useBackendAPI';
+import snakecaseKeys from 'snakecase-keys';
+
+const useDecisionProposalDraftMutation = (
+ application: Application
+): UseMutationResult<
+ DecisionProposalDraftData,
+ Error,
+ DecisionProposalDraft
+> => {
+ const { axios, handleResponse } = useBackendAPI();
+ const queryClient = useQueryClient();
+ const { t } = useTranslation();
+
+ return useMutation(
+ 'changeDecisionProposal',
+ (decisionProposalPayload: DecisionProposalDraft) =>
+ !application.id
+ ? Promise.reject(new Error('Missing application id'))
+ : handleResponse(
+ axios.patch(`${BackendEndpoint.DECISION_PROPOSAL_DRAFT}`, {
+ ...snakecaseKeys(decisionProposalPayload),
+ })
+ ),
+ {
+ onSuccess: () => {
+ void queryClient.invalidateQueries('applications');
+ void queryClient.invalidateQueries('application');
+ },
+ onError: (error: AxiosError>) => {
+ const errorData = camelcaseKeys(error.response?.data ?? {});
+ const isContentTypeHTML = typeof errorData === 'string';
+ hdsToast({
+ autoDismissTime: 0,
+ type: 'error',
+ labelText: t('common:error.generic.label'),
+ text: isContentTypeHTML
+ ? t('common:error.generic.text')
+ : Object.entries(errorData).map(([key, value]) =>
+ typeof value === 'string' ? (
+
+ {value}
+
+ ) : (
+ prettyPrintObject({ data: value as Record })
+ )
+ ),
+ });
+ },
+ }
+ );
+};
+
+export default useDecisionProposalDraftMutation;
diff --git a/frontend/benefit/handler/src/hooks/applicationHandling/useDecisionProposalTemplateQuery.ts b/frontend/benefit/handler/src/hooks/applicationHandling/useDecisionProposalTemplateQuery.ts
new file mode 100644
index 0000000000..ac344af410
--- /dev/null
+++ b/frontend/benefit/handler/src/hooks/applicationHandling/useDecisionProposalTemplateQuery.ts
@@ -0,0 +1,27 @@
+import { DecisionProposalTemplateData } from 'benefit/handler/types/common';
+import { BackendEndpoint } from 'benefit-shared/backend-api/backend-api';
+import { useQuery, UseQueryResult } from 'react-query';
+import useBackendAPI from 'shared/hooks/useBackendAPI';
+
+const useDecisionProposalTemplateQuery = (
+ id: string,
+ decisionType: string
+): UseQueryResult => {
+ const { axios, handleResponse } = useBackendAPI();
+ return useQuery(
+ ['decisionProposalTemplates'],
+ () =>
+ !id
+ ? Promise.reject(new Error('Missing application id'))
+ : handleResponse(
+ axios.get(`${String(BackendEndpoint.DECISION_PROPOSAL_TEMPLATE)}`, {
+ params: {
+ decision_type: decisionType,
+ application_id: id,
+ },
+ })
+ )
+ );
+};
+
+export default useDecisionProposalTemplateQuery;
diff --git a/frontend/benefit/handler/src/hooks/applicationHandling/useHandlingStepper.ts b/frontend/benefit/handler/src/hooks/applicationHandling/useHandlingStepper.ts
new file mode 100644
index 0000000000..cb496402b3
--- /dev/null
+++ b/frontend/benefit/handler/src/hooks/applicationHandling/useHandlingStepper.ts
@@ -0,0 +1,127 @@
+import { StepState } from 'hds-react';
+import { TFunction } from 'next-i18next';
+import { useReducer } from 'react';
+import { QueryClient } from 'react-query';
+
+type ApplicationStepperProps = {
+ stepState: StepStateType;
+ stepperDispatch: React.Dispatch;
+ getStepperHeading: (activeStepIndex: number) => string;
+};
+
+interface UpdateAction {
+ type: 'completeStep' | 'setActive';
+ payload: number;
+}
+
+export type Steps = {
+ label: string;
+ state: StepState;
+};
+
+export type StepStateType = {
+ activeStepIndex: number;
+ steps: Steps[];
+};
+
+export type StepActionType = {
+ type: 'completeStep' | 'setActive';
+ payload: number;
+};
+
+const mapCompleteStep = (
+ step: Steps,
+ stepsTotal: number,
+ index: number,
+ action: UpdateAction,
+ queryClient: QueryClient
+): Steps => {
+ if (index === action.payload && index !== stepsTotal - 1) {
+ // current one but not last one
+ return {
+ state: StepState.completed,
+ label: step.label,
+ };
+ }
+ if (index === action.payload + 1) {
+ // next one
+ return {
+ state: StepState.available,
+ label: step.label,
+ };
+ }
+ void queryClient.invalidateQueries('applications');
+ void queryClient.invalidateQueries('application');
+ return step;
+};
+
+const useApplicationStepper = (
+ id: string,
+ t: TFunction,
+ queryClient: QueryClient
+): ApplicationStepperProps => {
+ const commonReducer =
+ (stepsTotal: number) =>
+ (state: StepStateType, action: UpdateAction): StepStateType => {
+ switch (action.type) {
+ case 'completeStep':
+ return {
+ activeStepIndex:
+ action.payload === stepsTotal - 1
+ ? stepsTotal - 1
+ : action.payload + 1,
+ steps: state.steps.map((step, index: number) =>
+ mapCompleteStep(step, stepsTotal, index, action, queryClient)
+ ),
+ };
+
+ case 'setActive':
+ return {
+ activeStepIndex: action.payload,
+ steps: state.steps.map((step, index: number) => {
+ if (index === action.payload) {
+ void queryClient.invalidateQueries(['applications', id]);
+ return {
+ state: StepState.available,
+ label: step.label,
+ };
+ }
+ return step;
+ }),
+ };
+
+ default:
+ throw new Error('Cannot render stepper. Invalid action type.');
+ }
+ };
+ const reducer = commonReducer(3);
+ const initialState = {
+ activeStepIndex: 0,
+ steps: [
+ {
+ label: t('common:applications.steps.handlingProcess.step1'),
+ state: StepState.available,
+ },
+ {
+ label: t('common:applications.steps.handlingProcess.step2'),
+ state: StepState.disabled,
+ },
+ {
+ label: t('common:applications.steps.handlingProcess.step3'),
+ state: StepState.disabled,
+ },
+ ],
+ };
+ const [stepState, stepperDispatch] = useReducer(reducer, initialState);
+
+ const getStepperHeading = (activeStepIndex = 0): string =>
+ initialState.steps[activeStepIndex].label;
+
+ return {
+ stepState,
+ stepperDispatch,
+ getStepperHeading,
+ };
+};
+
+export { useApplicationStepper };
diff --git a/frontend/benefit/handler/src/hooks/useCalculatorData.ts b/frontend/benefit/handler/src/hooks/useCalculatorData.ts
index 8d9933fb2a..497783381e 100644
--- a/frontend/benefit/handler/src/hooks/useCalculatorData.ts
+++ b/frontend/benefit/handler/src/hooks/useCalculatorData.ts
@@ -17,13 +17,12 @@ type ExtendedComponentProps = {
handleSubmit: () => void;
handleClear: () => void;
getErrorMessage: (fieldName: string) => string | undefined;
- isRecalculationRequired: boolean;
- setIsRecalculationRequired: (value: boolean) => void;
};
const useCalculatorData = (
calculatorType: CALCULATION_TYPES,
- formik: FormikProps
+ formik: FormikProps,
+ setIsRecalculationRequired: (value: boolean) => void
): ExtendedComponentProps => {
const language = useLocale();
const theme = useTheme();
@@ -34,8 +33,6 @@ const useCalculatorData = (
const [isSubmitted, setIsSubmitted] = useState(false);
- const [isRecalculationRequired, setIsRecalculationRequired] = useState(false);
-
const getErrorMessage = (fieldName: string): string | undefined =>
getErrorText(errors, touched, fieldName, t, isSubmitted);
@@ -57,7 +54,7 @@ const useCalculatorData = (
useEffect(() => {
if (dirty) setIsRecalculationRequired(true);
- }, [dirty, values]);
+ }, [dirty, values, setIsRecalculationRequired]);
return {
t,
@@ -67,8 +64,6 @@ const useCalculatorData = (
getErrorMessage,
handleSubmit,
handleClear,
- isRecalculationRequired,
- setIsRecalculationRequired,
};
};
diff --git a/frontend/benefit/handler/src/hooks/useDetermineAhjoMode.ts b/frontend/benefit/handler/src/hooks/useDetermineAhjoMode.ts
new file mode 100644
index 0000000000..684aff036d
--- /dev/null
+++ b/frontend/benefit/handler/src/hooks/useDetermineAhjoMode.ts
@@ -0,0 +1,4 @@
+import { getLocalStorageItem } from 'shared/utils/localstorage.utils';
+
+export const useDetermineAhjoMode = (): boolean =>
+ !!getLocalStorageItem('newAhjoMode');
diff --git a/frontend/benefit/handler/src/hooks/useFormActions.tsx b/frontend/benefit/handler/src/hooks/useFormActions.tsx
index e0a8ebbbc4..d87196d152 100644
--- a/frontend/benefit/handler/src/hooks/useFormActions.tsx
+++ b/frontend/benefit/handler/src/hooks/useFormActions.tsx
@@ -351,7 +351,9 @@ const useFormActions = (
if (isFormActionNew) {
dispatchStep({ type: 'completeStep', payload: activeStep });
} else {
- void router.push(`${ROUTES.APPLICATION}?id=${applicationId}&updated=1`);
+ void router.push(
+ `${ROUTES.APPLICATION}?id=${applicationId}&action=update`
+ );
}
return result;
diff --git a/frontend/benefit/handler/src/hooks/useHandlerReviewActions.ts b/frontend/benefit/handler/src/hooks/useHandlerReviewActions.ts
index 1f0afb6d32..e489be682d 100644
--- a/frontend/benefit/handler/src/hooks/useHandlerReviewActions.ts
+++ b/frontend/benefit/handler/src/hooks/useHandlerReviewActions.ts
@@ -10,7 +10,7 @@ import camelcaseKeys from 'camelcase-keys';
import isEmpty from 'lodash/isEmpty';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
-import React, { useEffect, useState } from 'react';
+import React, { useEffect } from 'react';
import { convertToBackendDateFormat } from 'shared/utils/date.utils';
import { stringToFloatValue } from 'shared/utils/string.utils';
import snakecaseKeys from 'snakecase-keys';
@@ -23,17 +23,14 @@ interface HandlerReviewActions {
onSaveAndClose: () => void;
onDone: () => void;
onCancel: (cancelledApplication: HandledAplication) => void;
- calculationsErrors: ErrorData | undefined | null;
calculateSalaryBenefit: (values: CalculationFormProps) => void;
}
const useHandlerReviewActions = (
- application: Application
+ application: Application,
+ setCalculationErrors?: React.Dispatch>
): HandlerReviewActions => {
const updateApplicationQuery = useUpdateApplicationQuery();
- const [calculationsErrors, setCalculationErrors] = useState<
- ErrorData | undefined | null
- >();
const router = useRouter();
@@ -163,6 +160,7 @@ const useHandlerReviewActions = (
};
useEffect(() => {
+ if (!setCalculationErrors) return;
if (updateApplicationQuery.error) {
// Can parse error codes from data on 400s, set generic error on 500
if (
@@ -202,7 +200,6 @@ const useHandlerReviewActions = (
onDone,
onCancel,
calculateSalaryBenefit,
- calculationsErrors,
};
};
diff --git a/frontend/benefit/handler/src/hooks/useSteps.ts b/frontend/benefit/handler/src/hooks/useSteps.ts
index 1920c58ba7..0cf39c84fd 100644
--- a/frontend/benefit/handler/src/hooks/useSteps.ts
+++ b/frontend/benefit/handler/src/hooks/useSteps.ts
@@ -87,15 +87,15 @@ export const useSteps = (id: string | null): ExtendedComponentProps => {
activeStepIndex: activeStep,
steps: [
{
- label: t('common:applications.steps.step1'),
+ label: t('common:applications.steps.newApplication.step1'),
state: activeStep === 0 ? StepState.available : StepState.completed,
},
{
- label: t('common:applications.steps.step2'),
+ label: t('common:applications.steps.newApplication.step2'),
state: activeStep === 0 ? StepState.disabled : StepState.available,
},
{
- label: t('common:applications.steps.step3'),
+ label: t('common:applications.steps.newApplication.step3'),
state: StepState.disabled,
},
],
diff --git a/frontend/benefit/handler/src/hooks/useUpdateApplicationQuery.ts b/frontend/benefit/handler/src/hooks/useUpdateApplicationQuery.ts
index b119820cc3..775b9e6617 100644
--- a/frontend/benefit/handler/src/hooks/useUpdateApplicationQuery.ts
+++ b/frontend/benefit/handler/src/hooks/useUpdateApplicationQuery.ts
@@ -1,6 +1,8 @@
import { AxiosError } from 'axios';
import { BackendEndpoint } from 'benefit-shared/backend-api/backend-api';
+import { APPLICATION_STATUSES } from 'benefit-shared/constants';
import { ApplicationData } from 'benefit-shared/types/application';
+import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import { useMutation, UseMutationResult, useQueryClient } from 'react-query';
import showErrorToast from 'shared/components/toast/show-error-toast';
@@ -16,6 +18,7 @@ const useUpdateApplicationQuery = (): UseMutationResult<
const { axios, handleResponse } = useBackendAPI();
const { t } = useTranslation();
const queryClient = useQueryClient();
+ const router = useRouter();
return useMutation, ApplicationData>(
'updateApplication',
@@ -27,9 +30,17 @@ const useUpdateApplicationQuery = (): UseMutationResult<
)
),
{
- onSuccess: () => {
+ onSuccess: (response: ApplicationData) => {
void queryClient.invalidateQueries('applications');
void queryClient.invalidateQueries('application');
+ if (
+ [
+ APPLICATION_STATUSES.ACCEPTED,
+ APPLICATION_STATUSES.REJECTED,
+ ].includes(response.status)
+ ) {
+ void router.push(`/application?id=${response.id}&action=submit`);
+ }
},
onError: (error: AxiosError) => {
const errorStatus = error.code;
diff --git a/frontend/benefit/handler/src/pages/_app.tsx b/frontend/benefit/handler/src/pages/_app.tsx
index 220e6ed290..8f08924e99 100644
--- a/frontend/benefit/handler/src/pages/_app.tsx
+++ b/frontend/benefit/handler/src/pages/_app.tsx
@@ -1,8 +1,9 @@
-import 'react-toastify/dist/ReactToastify.css';
+import '../styles/tables.css';
import '../styles/tabs.css';
import 'benefit-shared/styles/app.css';
-import '../styles/tables.css';
import 'hds-design-tokens';
+import 'react-loading-skeleton/dist/skeleton.css';
+import 'react-toastify/dist/ReactToastify.css';
import AuthProvider from 'benefit/handler/auth/AuthProvider';
import Footer from 'benefit/handler/components/footer/Footer';
diff --git a/frontend/benefit/handler/src/pages/application/new/index.tsx b/frontend/benefit/handler/src/pages/application/new/index.tsx
index 6eba75b014..c26dd314e3 100644
--- a/frontend/benefit/handler/src/pages/application/new/index.tsx
+++ b/frontend/benefit/handler/src/pages/application/new/index.tsx
@@ -27,7 +27,7 @@ const NewApplication: NextPage = () => {
return (
- ;
+
);
};
diff --git a/frontend/benefit/handler/src/styles/stepper.css b/frontend/benefit/handler/src/styles/stepper.css
new file mode 100644
index 0000000000..ef6b43cbe6
--- /dev/null
+++ b/frontend/benefit/handler/src/styles/stepper.css
@@ -0,0 +1,3 @@
+.custom-stepper-heading {
+ font-size: var(--fontsize-heading-l);
+}
diff --git a/frontend/benefit/handler/src/types/application.d.ts b/frontend/benefit/handler/src/types/application.d.ts
index 94e1e7184e..3d3d5b31e4 100644
--- a/frontend/benefit/handler/src/types/application.d.ts
+++ b/frontend/benefit/handler/src/types/application.d.ts
@@ -2,6 +2,7 @@ import {
APPLICATION_ACTIONS,
APPLICATION_FIELD_KEYS,
} from 'benefit/handler/constants';
+import { ErrorData } from 'benefit/handler/types/common';
import {
APPLICATION_ORIGINS,
APPLICATION_STATUSES,
@@ -20,6 +21,7 @@ import {
Calculation,
CalculationCommon,
Company,
+ DecisionProposalDraft,
DeMinimisAid,
Employee,
PaySubsidy,
@@ -87,7 +89,11 @@ export interface UploadProps {
}
export interface SalaryBenefitCalculatorViewProps {
- data: Application;
+ application: Application;
+ isRecalculationRequired: boolean;
+ setIsRecalculationRequired: (value: boolean) => void;
+ setCalculationErrors: React.Dispatch>;
+ calculationsErrors: ErrorData | undefined | null;
}
export interface SalaryBenefitManualCalculatorViewProps {
@@ -99,12 +105,12 @@ export interface SalaryBenefitManualCalculatorViewProps {
}
export type HandledAplication = {
- status?:
- | APPLICATION_STATUSES.ACCEPTED
- | APPLICATION_STATUSES.REJECTED
- | APPLICATION_STATUSES.CANCELLED;
+ status?: APPLICATION_STATUSES;
logEntryComment?: string;
grantedAsDeMinimisAid?: boolean;
+ handlerRole?: string;
+ decisionText?: string;
+ justificationText?: string;
};
export type ApplicationChangesData = {
@@ -147,6 +153,7 @@ export type Application = {
action?: APPLICATION_ACTIONS;
changeReason?: string;
changes?: ApplicationChangesData;
+ decisionProposalDraft?: DecisionProposalDraft;
} & ApplicationForm;
export interface ApplicationForm {
diff --git a/frontend/benefit/handler/src/types/common.d.ts b/frontend/benefit/handler/src/types/common.d.ts
index 124810634a..2c62d9bda1 100644
--- a/frontend/benefit/handler/src/types/common.d.ts
+++ b/frontend/benefit/handler/src/types/common.d.ts
@@ -27,3 +27,20 @@ interface UploadErrorData {
non_field_errors?: string[];
[key: string]: string[] | undefined;
}
+
+export interface DecisionProposalTemplateData {
+ id: string;
+ name: string;
+ template_decision_text: string;
+ template_justification_text: string;
+}
+
+export interface DecisionProposalTemplate {
+ id: string;
+ name: string;
+ sectionType: string;
+ templateDecisionText: string;
+ templateJustificationText: string;
+}
+
+export type DecisionProposals = DecisionProposalTemplate[];
diff --git a/frontend/benefit/shared/src/backend-api/backend-api.ts b/frontend/benefit/shared/src/backend-api/backend-api.ts
index 2fa3a9a7f2..85d2fd5488 100644
--- a/frontend/benefit/shared/src/backend-api/backend-api.ts
+++ b/frontend/benefit/shared/src/backend-api/backend-api.ts
@@ -19,6 +19,8 @@ export const BackendEndpoint = {
SEARCH_ORGANISATION: '/v1/company/search/',
GET_ORGANISATION: '/v1/company/get/',
APPLICATION_ALTERATION: '/v1/applicationalterations/',
+ DECISION_PROPOSAL_TEMPLATE: 'v1/decision-proposal-sections/',
+ DECISION_PROPOSAL_DRAFT: 'v1/decision-proposal-drafts/',
} as const;
const singleBatchBase = (id: string): string =>
diff --git a/frontend/benefit/shared/src/constants.ts b/frontend/benefit/shared/src/constants.ts
index 4f8ed7da87..03c8cef3ac 100644
--- a/frontend/benefit/shared/src/constants.ts
+++ b/frontend/benefit/shared/src/constants.ts
@@ -220,3 +220,8 @@ export enum ALTERATION_STATE {
OPENED = 'opened',
HANDLED = 'handled',
}
+
+export enum DECISION_TYPES {
+ ACCEPTED = 'accepted_decision',
+ DENIED = 'denied_decision',
+}
diff --git a/frontend/benefit/shared/src/types/application.d.ts b/frontend/benefit/shared/src/types/application.d.ts
index 6cba8ffa8a..511266cf0c 100644
--- a/frontend/benefit/shared/src/types/application.d.ts
+++ b/frontend/benefit/shared/src/types/application.d.ts
@@ -227,6 +227,8 @@ export type Row = {
descriptionFi: string;
amount: string;
descriptionType: CALCULATION_ROW_DESCRIPTION_TYPES | null;
+ startDate?: string;
+ endDate?: string;
};
export type TrainingCompensation = {
@@ -238,6 +240,26 @@ export type TrainingCompensation = {
export type PaySubsidyPercent = typeof PAY_SUBSIDY_OPTIONS[number];
+export interface DecisionProposalDraftData {
+ handler_role?: string;
+ review_step?: number;
+ log_entry_comment?: string;
+ status?: APPLICATION_STATUSES;
+ decision_text?: string;
+ justification_text?: string;
+}
+
+export interface DecisionProposalDraft {
+ handlerRole?: string;
+ reviewStep?: number;
+ logEntryComment?: string;
+ grantedAsDeMinimisAid?: boolean;
+ status?: APPLICATION_STATUSES;
+ decisionText?: string;
+ justificationText?: string;
+ applicationId?: string;
+}
+
export type Application = {
id?: string;
status?: APPLICATION_STATUSES;
@@ -272,6 +294,8 @@ export type Application = {
ahjoDecisionDate?: string;
calculatedBenefitAmount?: number;
alterations?: Array;
+ decisionProposalDraft?: DecisionProposalDraft;
+ applicationOrigin?: APPLICATION_ORIGINS;
} & Step1 &
Step2;
diff --git a/frontend/shared/src/components/drawer/Drawer.sc.tsx b/frontend/shared/src/components/drawer/Drawer.sc.tsx
index 1f0167470d..d8a8cf6409 100644
--- a/frontend/shared/src/components/drawer/Drawer.sc.tsx
+++ b/frontend/shared/src/components/drawer/Drawer.sc.tsx
@@ -1,12 +1,16 @@
import styled from 'styled-components';
-export const $Drawer = styled.aside`
+type DrawerProps = {
+ stickyBarInUse?: boolean;
+};
+
+export const $Drawer = styled.aside`
position: fixed;
top: 0;
right: 0;
bottom: 0;
width: 520px;
- height: 100vh;
+ height: calc(100vh - ${(props) => (props.stickyBarInUse ? '80' : '0')}px);
background-color: ${({ theme }) => theme.colors.white};
z-index: 100;
box-shadow: -2px 0px 10px 0px rgb(0 0 0 / 10%);
diff --git a/frontend/shared/src/components/drawer/Drawer.tsx b/frontend/shared/src/components/drawer/Drawer.tsx
index 6ff4dfe59a..11fd22e31a 100644
--- a/frontend/shared/src/components/drawer/Drawer.tsx
+++ b/frontend/shared/src/components/drawer/Drawer.tsx
@@ -9,6 +9,7 @@ export type DrawerProps = {
footer?: React.ReactNode;
isOpen: boolean;
onClose?: () => void;
+ stickyBarInUse?: boolean;
};
const Drawer: React.FC = ({
@@ -18,11 +19,12 @@ const Drawer: React.FC = ({
footer,
closeText,
onClose,
+ stickyBarInUse,
}) => {
if (!isOpen) return null;
return (
- <$Drawer>
+ <$Drawer stickyBarInUse={stickyBarInUse}>
{title && (
<$Top>
<$Title>{title}$Title>
diff --git a/frontend/shared/src/components/forms/heading/Heading.sc.ts b/frontend/shared/src/components/forms/heading/Heading.sc.ts
index f815fa2487..c2d093287d 100644
--- a/frontend/shared/src/components/forms/heading/Heading.sc.ts
+++ b/frontend/shared/src/components/forms/heading/Heading.sc.ts
@@ -1,4 +1,4 @@
-import styled, { DefaultTheme } from 'styled-components';
+import styled, { CSSProp, DefaultTheme } from 'styled-components';
export type HeadingProps = {
size?: keyof DefaultTheme['fontSize']['heading'];
@@ -7,8 +7,10 @@ export type HeadingProps = {
loadingText?: string;
loadingFinishedText?: string;
tooltip?: string;
- as?: 'h1' | 'h2' | 'h3' | 'h4' | 'div';
+ as?: 'h1' | 'h2' | 'h3' | 'h4' | 'p' | 'div';
'data-testid'?: string;
+ weight?: string;
+ $css?: CSSProp;
};
export const $Header = styled.h1`
@@ -18,5 +20,5 @@ export const $Header = styled.h1`
display: flex;
align-items: center;
gap: ${(props) => props.theme.spacing.s};
- font-weight: 500;
+ font-weight: ${(props) => (props.weight ? props.weight : '500')};
`;
diff --git a/frontend/shared/src/components/forms/heading/Heading.tsx b/frontend/shared/src/components/forms/heading/Heading.tsx
index 464af1fab3..2b70a67073 100644
--- a/frontend/shared/src/components/forms/heading/Heading.tsx
+++ b/frontend/shared/src/components/forms/heading/Heading.tsx
@@ -11,10 +11,18 @@ const Heading: React.FC = ({
loading,
tooltip,
'data-testid': dataTestId,
+ weight,
+ $css,
}) => {
const { t } = useTranslation();
return (
- <$Header size={size} as={as} data-testid={dataTestId}>
+ <$Header
+ size={size}
+ as={as}
+ data-testid={dataTestId}
+ weight={weight}
+ css={$css}
+ >
{header}
{tooltip && (
`
export const $Grid = styled.div.attrs<
GridProps,
- Omit & { columnGap: SpacingValue, rowGap: SpacingValue }
+ Omit & { columnGap: SpacingValue; rowGap: SpacingValue }
>((props) => ({
...props,
rowGap: props.rowGap || props.gap || props.theme.spacing.m,
@@ -81,7 +81,9 @@ export const $Grid = styled.div.attrs<
left: calc(-1 * ${bgHorizontalPadding ? gap : '0px'});
right: calc(-1 * ${bgHorizontalPadding ? gap : '0px'});
bottom: calc(-1 * ${bgVerticalPadding ? gap : '0px'});
- background-color: ${theme.colors.black5};
+ background-color: ${
+ typeof bgColor === 'boolean' ? theme.colors.black5 : bgColor
+ };
}
`}
diff --git a/frontend/shared/src/styles/theme.ts b/frontend/shared/src/styles/theme.ts
index bf0e905c68..da43e90110 100644
--- a/frontend/shared/src/styles/theme.ts
+++ b/frontend/shared/src/styles/theme.ts
@@ -7,6 +7,7 @@ const tokens = {
const componentColors = {
stepper: {
+ coat: tokens.coatOfArms,
black: 'var(--color-black-90)',
white: 'var(--color-white)',
grey: 'var(--color-black-30)',
@@ -170,13 +171,24 @@ const theme: DefaultTheme = {
'--header-background-color': tokens.coatOfArms,
},
stepper: {
- '--hds-not-selected-step-label-color': componentColors.stepper.black,
- '--hds-step-background-color': componentColors.stepper.white,
- '--hds-step-content-color': componentColors.stepper.black,
- '--hds-stepper-background-color': componentColors.stepper.white,
- '--hds-stepper-color': componentColors.stepper.black,
- '--hds-stepper-disabled-color': componentColors.stepper.grey,
- '--hds-stepper-focus-border-color': componentColors.stepper.black,
+ coat: {
+ '--hds-not-selected-step-label-color': componentColors.stepper.coat,
+ '--hds-step-background-color': componentColors.stepper.white,
+ '--hds-step-content-color': componentColors.stepper.coat,
+ '--hds-stepper-background-color': componentColors.stepper.white,
+ '--hds-stepper-color': componentColors.stepper.coat,
+ '--hds-stepper-disabled-color': componentColors.stepper.grey,
+ '--hds-stepper-focus-border-color': componentColors.stepper.black,
+ },
+ black: {
+ '--hds-not-selected-step-label-color': componentColors.stepper.black,
+ '--hds-step-background-color': componentColors.stepper.white,
+ '--hds-step-content-color': componentColors.stepper.black,
+ '--hds-stepper-background-color': componentColors.stepper.white,
+ '--hds-stepper-color': componentColors.stepper.black,
+ '--hds-stepper-disabled-color': componentColors.stepper.grey,
+ '--hds-stepper-focus-border-color': componentColors.stepper.black,
+ },
},
radio: {
'--border-color-selected': componentColors.radio.base,
diff --git a/frontend/shared/src/types/styled-components.d.ts b/frontend/shared/src/types/styled-components.d.ts
index fb689a35ef..4d4b71dd05 100644
--- a/frontend/shared/src/types/styled-components.d.ts
+++ b/frontend/shared/src/types/styled-components.d.ts
@@ -1,5 +1,14 @@
import 'styled-components';
+type StepperTheme = {
+ '--hds-not-selected-step-label-color': string;
+ '--hds-step-background-color': string;
+ '--hds-step-content-color': string;
+ '--hds-stepper-background-color': string;
+ '--hds-stepper-color': string;
+ '--hds-stepper-disabled-color': string;
+ '--hds-stepper-focus-border-color': string;
+};
declare module 'styled-components' {
export interface DefaultTheme {
colors: {
@@ -143,13 +152,8 @@ declare module 'styled-components' {
};
components: {
stepper: {
- '--hds-not-selected-step-label-color': string;
- '--hds-step-background-color': string;
- '--hds-step-content-color': string;
- '--hds-stepper-background-color': string;
- '--hds-stepper-color': string;
- '--hds-stepper-disabled-color': string;
- '--hds-stepper-focus-border-color': string;
+ coat: StepperTheme;
+ black: StepperTheme;
};
tabs: {
'--tab-color': string;
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index c8b17ac5ff..0957517e49 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -2657,6 +2657,11 @@
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.5.tgz#db5a11bf66bdab39569719555b0f76e138d7bd64"
integrity sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw==
+"@popperjs/core@^2.9.0":
+ version "2.11.8"
+ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f"
+ integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
+
"@reach/observe-rect@^1.1.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@reach/observe-rect/-/observe-rect-1.2.0.tgz#d7a6013b8aafcc64c778a0ccb83355a11204d3b2"
@@ -2940,6 +2945,11 @@
resolved "https://registry.yarnpkg.com/@react-types/shared/-/shared-3.22.0.tgz#70f85aad46cd225f7fcb29f1c2b5213163605074"
integrity sha512-yVOekZWbtSmmiThGEIARbBpnmUIuePFlLyctjvCbgJgGhz8JnEJOipLQ/a4anaWfzAgzSceQP8j/K+VOOePleA==
+"@remirror/core-constants@^2.0.2":
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/@remirror/core-constants/-/core-constants-2.0.2.tgz#f05eccdc69e3a65e7d524b52548f567904a11a1a"
+ integrity sha512-dyHY+sMF0ihPus3O27ODd4+agdHMEmuRdyiZJ2CCWjPV5UFmn17ZbElvk6WOGVE4rdCJKZQCrPV2BcikOMLUGQ==
+
"@rollup/plugin-sucrase@4.0.4":
version "4.0.4"
resolved "https://registry.yarnpkg.com/@rollup/plugin-sucrase/-/plugin-sucrase-4.0.4.tgz#0a3b3d97cdc239ec3399f5a10711f751e9f95d98"
@@ -3342,6 +3352,82 @@
resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.1.1.tgz#e1ff6118896e4b22af31e5ea2f9da956adde23d8"
integrity sha512-XrjH/iEUqNl9lF2HX9YhPNV7Amntkcnpw0Bo1KkRzowNDcgSN9i0nm4Q8Oi5wupgdfPaJNMAWa61A+voD6Kmwg==
+"@tiptap/core@^2.2.4":
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-2.2.4.tgz#6f957678eb733e70b9282fb5098d284a77bd09a3"
+ integrity sha512-cRrI8IlLIhCE1hacBQzXIC8dsRvGq6a4lYWQK/BaHuZg21CG7szp3Vd8Ix+ra1f5v0xPOT+Hy+QFNQooRMKMCw==
+
+"@tiptap/extension-bubble-menu@^2.2.4":
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.2.4.tgz#2477901c78a18c629f7ae828394ff1f91a500c60"
+ integrity sha512-Nx1fS9jcFlhxaTDYlnayz2UulhK6CMaePc36+7PQIVI+u20RhgTCRNr25zKNemvsiM0RPZZVUjlHkxC0l5as1Q==
+ dependencies:
+ tippy.js "^6.3.7"
+
+"@tiptap/extension-document@^2.2.4":
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-document/-/extension-document-2.2.4.tgz#d46c1fdd1ea5b7191112fae4e9f8a63a46309afc"
+ integrity sha512-z+05xGK0OFoXV1GL+/8bzcZuWMdMA3+EKwk5c+iziG60VZcvGTF7jBRsZidlu9Oaj0cDwWHCeeo6L9SgSh6i2A==
+
+"@tiptap/extension-floating-menu@^2.2.4":
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-floating-menu/-/extension-floating-menu-2.2.4.tgz#a97becec8cedfa1fb282a7a52caed35a80098581"
+ integrity sha512-U25l7PEzOmlAPugNRl8t8lqyhQZS6W/+3f92+FdwW9qXju3i62iX/3OGCC3Gv+vybmQ4fbZmMjvl+VDfenNi3A==
+ dependencies:
+ tippy.js "^6.3.7"
+
+"@tiptap/extension-heading@^2.2.4":
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-heading/-/extension-heading-2.2.4.tgz#a90469a879713dc074ce45e3d7d8cf7a02ae6a1f"
+ integrity sha512-gkq7Ns2FcrOCRq7Q+VRYt5saMt2R9g4REAtWy/jEevJ5UV5vA2AiGnYDmxwAkHutoYU0sAUkjqx37wE0wpamNw==
+
+"@tiptap/extension-history@^2.2.4":
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-history/-/extension-history-2.2.4.tgz#2d37ccfce9a8b7997bb4d46ebb19e986c5eb6f39"
+ integrity sha512-FDM32XYF5NU4mzh+fJ8w2CyUqv0l2Nl15sd6fOhQkVxSj8t57z+DUXc9ZR3zkH+1RAagYJo/2Gu3e99KpMr0tg==
+
+"@tiptap/extension-paragraph@^2.2.4":
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-paragraph/-/extension-paragraph-2.2.4.tgz#9126fafbf984e324bfb3fab34deb689def7eb98d"
+ integrity sha512-m1KwyvTNJxsq7StbspbcOhxO4Wk4YpElDbqOouWi+H4c8azdpI5Pn96ZqhFeE9bSyjByg6OcB/wqoJsLbeFWdQ==
+
+"@tiptap/extension-text@^2.2.4":
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/@tiptap/extension-text/-/extension-text-2.2.4.tgz#aa101c568aa78a4ddc06a944eefcd3ac944987d4"
+ integrity sha512-NlKHMPnRJXB+0AGtDlU0P2Pg+SdesA2lMMd7JzDUgJgL7pX2jOb8eUqSeOjFKuSzFSqYfH6C3o6mQiNhuQMv+g==
+
+"@tiptap/pm@^2.2.4":
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/@tiptap/pm/-/pm-2.2.4.tgz#701975e3221ac40b1bfba52d89e1345024212411"
+ integrity sha512-Po0klR165zgtinhVp1nwMubjyKx6gAY9kH3IzcniYLCkqhPgiqnAcCr61TBpp4hfK8YURBS4ihvCB1dyfCyY8A==
+ dependencies:
+ prosemirror-changeset "^2.2.1"
+ prosemirror-collab "^1.3.1"
+ prosemirror-commands "^1.5.2"
+ prosemirror-dropcursor "^1.8.1"
+ prosemirror-gapcursor "^1.3.2"
+ prosemirror-history "^1.3.2"
+ prosemirror-inputrules "^1.3.0"
+ prosemirror-keymap "^1.2.2"
+ prosemirror-markdown "^1.12.0"
+ prosemirror-menu "^1.2.4"
+ prosemirror-model "^1.19.4"
+ prosemirror-schema-basic "^1.2.2"
+ prosemirror-schema-list "^1.3.0"
+ prosemirror-state "^1.4.3"
+ prosemirror-tables "^1.3.5"
+ prosemirror-trailing-node "^2.0.7"
+ prosemirror-transform "^1.8.0"
+ prosemirror-view "^1.32.7"
+
+"@tiptap/react@^2.2.4":
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/@tiptap/react/-/react-2.2.4.tgz#bfb6a484a26d85df7a6f9636b98c38f4e83de4c0"
+ integrity sha512-HkYmMZWcETPZn3KpzdDg/ns2TKeFh54TvtCEInA4ljYtWGLoZc/A+KaiEtMIgVs+Mo1XwrhuoNGjL9c0OK2HJw==
+ dependencies:
+ "@tiptap/extension-bubble-menu" "^2.2.4"
+ "@tiptap/extension-floating-menu" "^2.2.4"
+
"@tootallnate/once@1":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
@@ -5743,6 +5829,11 @@ create-require@^1.1.0:
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
+crelt@^1.0.0:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72"
+ integrity sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==
+
cross-env@^7.0.3:
version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf"
@@ -10329,6 +10420,13 @@ lines-and-columns@~2.0.3:
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-2.0.4.tgz#d00318855905d2660d8c0822e3f5a4715855fc42"
integrity sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A==
+linkify-it@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-5.0.0.tgz#9ef238bfa6dc70bd8e7f9572b52d369af569b421"
+ integrity sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==
+ dependencies:
+ uc.micro "^2.0.0"
+
lint-staged@^12.3.1:
version "12.3.7"
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-12.3.7.tgz#ad0e2014302f704f9cf2c0ebdb97ac63d0f17be0"
@@ -10678,6 +10776,18 @@ map-stream@0.0.7:
resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.0.7.tgz#8a1f07896d82b10926bd3744a2420009f88974a8"
integrity sha1-ih8HiW2CsQkmvTdEokIACfiJdKg=
+markdown-it@^14.0.0:
+ version "14.0.0"
+ resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-14.0.0.tgz#b4b2ddeb0f925e88d981f84c183b59bac9e3741b"
+ integrity sha512-seFjF0FIcPt4P9U39Bq1JYblX0KZCjDLFFQPHpL5AzHpqPEKtosxmdq/LTVZnjfH7tjt9BxStm+wXcDBNuYmzw==
+ dependencies:
+ argparse "^2.0.1"
+ entities "^4.4.0"
+ linkify-it "^5.0.0"
+ mdurl "^2.0.0"
+ punycode.js "^2.3.1"
+ uc.micro "^2.0.0"
+
match-sorter@^6.0.2:
version "6.3.1"
resolved "https://registry.yarnpkg.com/match-sorter/-/match-sorter-6.3.1.tgz#98cc37fda756093424ddf3cbc62bfe9c75b92bda"
@@ -10762,6 +10872,11 @@ mdn-data@2.0.14:
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50"
integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==
+mdurl@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-2.0.0.tgz#80676ec0433025dd3e17ee983d0fe8de5a2237e0"
+ integrity sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==
+
media-engine@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/media-engine/-/media-engine-1.0.3.tgz#be3188f6cd243ea2a40804a35de5a5b032f58dad"
@@ -12399,6 +12514,11 @@ ora@^5.4.1:
strip-ansi "^6.0.0"
wcwidth "^1.0.1"
+orderedmap@^2.0.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/orderedmap/-/orderedmap-2.1.1.tgz#61481269c44031c449915497bf5a4ad273c512d2"
+ integrity sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==
+
os-family@^1.0.0, os-family@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/os-family/-/os-family-1.1.0.tgz#8a89cb617dd1631b8ef9506be830144f626c214e"
@@ -12991,6 +13111,159 @@ property-information@^6.0.0:
resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.3.0.tgz#ba4a06ec6b4e1e90577df9931286953cdf4282c3"
integrity sha512-gVNZ74nqhRMiIUYWGQdosYetaKc83x8oT41a0LlV3AAFCAZwCpg4vmGkq8t34+cUhp3cnM4XDiU/7xlgK7HGrg==
+prosemirror-changeset@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/prosemirror-changeset/-/prosemirror-changeset-2.2.1.tgz#dae94b63aec618fac7bb9061648e6e2a79988383"
+ integrity sha512-J7msc6wbxB4ekDFj+n9gTW/jav/p53kdlivvuppHsrZXCaQdVgRghoZbSS3kwrRyAstRVQ4/+u5k7YfLgkkQvQ==
+ dependencies:
+ prosemirror-transform "^1.0.0"
+
+prosemirror-collab@^1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz#0e8c91e76e009b53457eb3b3051fb68dad029a33"
+ integrity sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==
+ dependencies:
+ prosemirror-state "^1.0.0"
+
+prosemirror-commands@^1.0.0, prosemirror-commands@^1.5.2:
+ version "1.5.2"
+ resolved "https://registry.yarnpkg.com/prosemirror-commands/-/prosemirror-commands-1.5.2.tgz#e94aeea52286f658cd984270de9b4c3fff580852"
+ integrity sha512-hgLcPaakxH8tu6YvVAaILV2tXYsW3rAdDR8WNkeKGcgeMVQg3/TMhPdVoh7iAmfgVjZGtcOSjKiQaoeKjzd2mQ==
+ dependencies:
+ prosemirror-model "^1.0.0"
+ prosemirror-state "^1.0.0"
+ prosemirror-transform "^1.0.0"
+
+prosemirror-dropcursor@^1.8.1:
+ version "1.8.1"
+ resolved "https://registry.yarnpkg.com/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.1.tgz#49b9fb2f583e0d0f4021ff87db825faa2be2832d"
+ integrity sha512-M30WJdJZLyXHi3N8vxN6Zh5O8ZBbQCz0gURTfPmTIBNQ5pxrdU7A58QkNqfa98YEjSAL1HUyyU34f6Pm5xBSGw==
+ dependencies:
+ prosemirror-state "^1.0.0"
+ prosemirror-transform "^1.1.0"
+ prosemirror-view "^1.1.0"
+
+prosemirror-gapcursor@^1.3.2:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/prosemirror-gapcursor/-/prosemirror-gapcursor-1.3.2.tgz#5fa336b83789c6199a7341c9493587e249215cb4"
+ integrity sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ==
+ dependencies:
+ prosemirror-keymap "^1.0.0"
+ prosemirror-model "^1.0.0"
+ prosemirror-state "^1.0.0"
+ prosemirror-view "^1.0.0"
+
+prosemirror-history@^1.0.0, prosemirror-history@^1.3.2:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/prosemirror-history/-/prosemirror-history-1.3.2.tgz#ce6ad7ab9db83e761aee716f3040d74738311b15"
+ integrity sha512-/zm0XoU/N/+u7i5zepjmZAEnpvjDtzoPWW6VmKptcAnPadN/SStsBjMImdCEbb3seiNTpveziPTIrXQbHLtU1g==
+ dependencies:
+ prosemirror-state "^1.2.2"
+ prosemirror-transform "^1.0.0"
+ prosemirror-view "^1.31.0"
+ rope-sequence "^1.3.0"
+
+prosemirror-inputrules@^1.3.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/prosemirror-inputrules/-/prosemirror-inputrules-1.4.0.tgz#ef1519bb2cb0d1e0cec74bad1a97f1c1555068bb"
+ integrity sha512-6ygpPRuTJ2lcOXs9JkefieMst63wVJBgHZGl5QOytN7oSZs3Co/BYbc3Yx9zm9H37Bxw8kVzCnDsihsVsL4yEg==
+ dependencies:
+ prosemirror-state "^1.0.0"
+ prosemirror-transform "^1.0.0"
+
+prosemirror-keymap@^1.0.0, prosemirror-keymap@^1.1.2, prosemirror-keymap@^1.2.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-1.2.2.tgz#14a54763a29c7b2704f561088ccf3384d14eb77e"
+ integrity sha512-EAlXoksqC6Vbocqc0GtzCruZEzYgrn+iiGnNjsJsH4mrnIGex4qbLdWWNza3AW5W36ZRrlBID0eM6bdKH4OStQ==
+ dependencies:
+ prosemirror-state "^1.0.0"
+ w3c-keyname "^2.2.0"
+
+prosemirror-markdown@^1.12.0:
+ version "1.12.0"
+ resolved "https://registry.yarnpkg.com/prosemirror-markdown/-/prosemirror-markdown-1.12.0.tgz#d2de09d37897abf7adb6293d925ff132dac5b0a6"
+ integrity sha512-6F5HS8Z0HDYiS2VQDZzfZP6A0s/I0gbkJy8NCzzDMtcsz3qrfqyroMMeoSjAmOhDITyon11NbXSzztfKi+frSQ==
+ dependencies:
+ markdown-it "^14.0.0"
+ prosemirror-model "^1.0.0"
+
+prosemirror-menu@^1.2.4:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/prosemirror-menu/-/prosemirror-menu-1.2.4.tgz#3cfdc7c06d10f9fbd1bce29082c498bd11a0a79a"
+ integrity sha512-S/bXlc0ODQup6aiBbWVsX/eM+xJgCTAfMq/nLqaO5ID/am4wS0tTCIkzwytmao7ypEtjj39i7YbJjAgO20mIqA==
+ dependencies:
+ crelt "^1.0.0"
+ prosemirror-commands "^1.0.0"
+ prosemirror-history "^1.0.0"
+ prosemirror-state "^1.0.0"
+
+prosemirror-model@^1.0.0, prosemirror-model@^1.16.0, prosemirror-model@^1.19.0, prosemirror-model@^1.19.4, prosemirror-model@^1.8.1:
+ version "1.19.4"
+ resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.19.4.tgz#e45e84480c97dd3922095dbe579e1c98c86c0704"
+ integrity sha512-RPmVXxUfOhyFdayHawjuZCxiROsm9L4FCUA6pWI+l7n2yCBsWy9VpdE1hpDHUS8Vad661YLY9AzqfjLhAKQ4iQ==
+ dependencies:
+ orderedmap "^2.0.0"
+
+prosemirror-schema-basic@^1.2.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.2.tgz#6695f5175e4628aab179bf62e5568628b9cfe6c7"
+ integrity sha512-/dT4JFEGyO7QnNTe9UaKUhjDXbTNkiWTq/N4VpKaF79bBjSExVV2NXmJpcM7z/gD7mbqNjxbmWW5nf1iNSSGnw==
+ dependencies:
+ prosemirror-model "^1.19.0"
+
+prosemirror-schema-list@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/prosemirror-schema-list/-/prosemirror-schema-list-1.3.0.tgz#05374702cf35a3ba5e7ec31079e355a488d52519"
+ integrity sha512-Hz/7gM4skaaYfRPNgr421CU4GSwotmEwBVvJh5ltGiffUJwm7C8GfN/Bc6DR1EKEp5pDKhODmdXXyi9uIsZl5A==
+ dependencies:
+ prosemirror-model "^1.0.0"
+ prosemirror-state "^1.0.0"
+ prosemirror-transform "^1.7.3"
+
+prosemirror-state@^1.0.0, prosemirror-state@^1.2.2, prosemirror-state@^1.3.1, prosemirror-state@^1.4.3:
+ version "1.4.3"
+ resolved "https://registry.yarnpkg.com/prosemirror-state/-/prosemirror-state-1.4.3.tgz#94aecf3ffd54ec37e87aa7179d13508da181a080"
+ integrity sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==
+ dependencies:
+ prosemirror-model "^1.0.0"
+ prosemirror-transform "^1.0.0"
+ prosemirror-view "^1.27.0"
+
+prosemirror-tables@^1.3.5:
+ version "1.3.7"
+ resolved "https://registry.yarnpkg.com/prosemirror-tables/-/prosemirror-tables-1.3.7.tgz#9d296bd432d2bc7dca90f14e5c3b5c5f61277f7a"
+ integrity sha512-oEwX1wrziuxMtwFvdDWSFHVUWrFJWt929kVVfHvtTi8yvw+5ppxjXZkMG/fuTdFo+3DXyIPSKfid+Be1npKXDA==
+ dependencies:
+ prosemirror-keymap "^1.1.2"
+ prosemirror-model "^1.8.1"
+ prosemirror-state "^1.3.1"
+ prosemirror-transform "^1.2.1"
+ prosemirror-view "^1.13.3"
+
+prosemirror-trailing-node@^2.0.7:
+ version "2.0.8"
+ resolved "https://registry.yarnpkg.com/prosemirror-trailing-node/-/prosemirror-trailing-node-2.0.8.tgz#233ddcbda72de06f9b5d758d2a65a8cac482ea10"
+ integrity sha512-ujRYhSuhQb1Jsarh1IHqb2KoSnRiD7wAMDGucP35DN7j5af6X7B18PfdPIrbwsPTqIAj0fyOvxbuPsWhNvylmA==
+ dependencies:
+ "@remirror/core-constants" "^2.0.2"
+ escape-string-regexp "^4.0.0"
+
+prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.2.1, prosemirror-transform@^1.7.3, prosemirror-transform@^1.8.0:
+ version "1.8.0"
+ resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.8.0.tgz#a47c64a3c373c1bd0ff46e95be3210c8dda0cd11"
+ integrity sha512-BaSBsIMv52F1BVVMvOmp1yzD3u65uC3HTzCBQV1WDPqJRQ2LuHKcyfn0jwqodo8sR9vVzMzZyI+Dal5W9E6a9A==
+ dependencies:
+ prosemirror-model "^1.0.0"
+
+prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.13.3, prosemirror-view@^1.27.0, prosemirror-view@^1.31.0, prosemirror-view@^1.32.7:
+ version "1.33.1"
+ resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.33.1.tgz#58dfd154f4fb1c9f7353bf1097c54d6afc6f57ea"
+ integrity sha512-62qkYgSJIkwIMMCpuGuPzc52DiK1Iod6TWoIMxP4ja6BTD4yO8kCUL64PZ/WhH/dJ9fW0CDO39FhH1EMyhUFEg==
+ dependencies:
+ prosemirror-model "^1.16.0"
+ prosemirror-state "^1.0.0"
+ prosemirror-transform "^1.1.0"
+
proto-list@~1.2.1:
version "1.2.4"
resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
@@ -13037,6 +13310,11 @@ pump@^3.0.0:
end-of-stream "^1.1.0"
once "^1.3.1"
+punycode.js@^2.3.1:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/punycode.js/-/punycode.js-2.3.1.tgz#6b53e56ad75588234e79f4affa90972c7dd8cdb7"
+ integrity sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==
+
punycode@^2.1.0, punycode@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
@@ -13804,6 +14082,11 @@ rollup@2.78.0:
optionalDependencies:
fsevents "~2.3.2"
+rope-sequence@^1.3.0:
+ version "1.3.4"
+ resolved "https://registry.yarnpkg.com/rope-sequence/-/rope-sequence-1.3.4.tgz#df85711aaecd32f1e756f76e43a415171235d425"
+ integrity sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==
+
rtl-css-js@^1.14.0:
version "1.15.0"
resolved "https://registry.yarnpkg.com/rtl-css-js/-/rtl-css-js-1.15.0.tgz#680ed816e570a9ebccba9e1cd0f202c6a8bb2dc0"
@@ -15140,6 +15423,13 @@ tiny-warning@^1.0.0, tiny-warning@^1.0.2:
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
+tippy.js@^6.3.7:
+ version "6.3.7"
+ resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.3.7.tgz#8ccfb651d642010ed9a32ff29b0e9e19c5b8c61c"
+ integrity sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==
+ dependencies:
+ "@popperjs/core" "^2.9.0"
+
tmp-promise@^1.0.5:
version "1.1.0"
resolved "https://registry.yarnpkg.com/tmp-promise/-/tmp-promise-1.1.0.tgz#bb924d239029157b9bc1d506a6aa341f8b13e64c"
@@ -15518,6 +15808,11 @@ typescript@^4.4.3, typescript@^4.5.5:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.3.tgz#eefeafa6afdd31d725584c67a0eaba80f6fc6c6c"
integrity sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==
+uc.micro@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-2.1.0.tgz#f8d3f7d0ec4c3dea35a7e3c8efa4cb8b45c9e7ee"
+ integrity sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==
+
uglify-js@^3.1.4, uglify-js@^3.5.1:
version "3.17.4"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.17.4.tgz#61678cf5fa3f5b7eb789bb345df29afb8257c22c"
@@ -15906,6 +16201,11 @@ w3c-hr-time@^1.0.2:
dependencies:
browser-process-hrtime "^1.0.0"
+w3c-keyname@^2.2.0:
+ version "2.2.8"
+ resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz#7b17c8c6883d4e8b86ac8aba79d39e880f8869c5"
+ integrity sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==
+
w3c-xmlserializer@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz#3e7104a05b75146cc60f564380b7f683acf1020a"