From 34e620124426afbfd232e73eb88c46de0b7d6d15 Mon Sep 17 00:00:00 2001 From: Sampo Tawast <5328394+sirtawast@users.noreply.github.com> Date: Thu, 7 Nov 2024 14:39:42 +0200 Subject: [PATCH] feat: add accordion to decision box to list instalments (hl-1497) (#3515) * chore: update translations * feat: expose instalment data to single application; validate instalment status data * fix: refactor and fix some details on instalment sums * feat: add a new accordion for instalments in the decision box * fix: add custom sort function for decision date (hl-1526) * feat: use modal for instalment cancel confirm and archive application when proceeding --- .../api/v1/serializers/application.py | 43 ++- .../benefit/calculator/api/v1/serializers.py | 8 + backend/benefit/calculator/api/v1/views.py | 11 +- .../handler/public/locales/en/common.json | 19 +- .../handler/public/locales/fi/common.json | 19 +- .../handler/public/locales/sv/common.json | 19 +- .../applicationList/ApplicationList.tsx | 18 +- .../ApplicationListForInstalments.tsx | 121 ++++---- .../DecisionCalculationAccordion.sc.ts | 6 +- .../DecisionCalculationAccordion.tsx | 284 ++++++++++++------ .../benefit/shared/src/types/application.d.ts | 13 +- 11 files changed, 375 insertions(+), 186 deletions(-) diff --git a/backend/benefit/applications/api/v1/serializers/application.py b/backend/benefit/applications/api/v1/serializers/application.py index 47530b7d33..f24bf5e054 100755 --- a/backend/benefit/applications/api/v1/serializers/application.py +++ b/backend/benefit/applications/api/v1/serializers/application.py @@ -92,6 +92,25 @@ from users.utils import get_company_from_request, get_request_user_from_context +def _get_pending_instalment(application, excluded_status=[]): + """Get the latest pending instalment for the application""" + try: + instalments = application.calculation.instalments.filter( + instalment_number__gt=1 + ) + instalment = ( + instalments.exclude(status__in=excluded_status) + .order_by("-due_date") + .first() + or None + ) + if instalment is not None: + return InstalmentSerializer(instalment).data + except AttributeError: + return None + return None + + class BaseApplicationSerializer(DynamicFieldsModelSerializer): """ Fields in the Company model come from YTJ/other source and are not editable by user, and are listed @@ -1638,6 +1657,11 @@ class HandlerApplicationSerializer(BaseApplicationSerializer): ahjo_error = serializers.SerializerMethodField() + pending_instalment = serializers.SerializerMethodField("get_pending_instalment") + + def get_pending_instalment(self, application): + return _get_pending_instalment(application) + def get_latest_ahjo_error(self, obj) -> Union[Dict, None]: """Get the latest Ahjo error for the application""" try: @@ -1698,11 +1722,13 @@ class Meta(BaseApplicationSerializer.Meta): "handler", "handled_by_ahjo_automation", "ahjo_error", + "pending_instalment", ] read_only_fields = BaseApplicationSerializer.Meta.read_only_fields + [ "latest_decision_comment", "handled_at", "handler", + "pending_instalment", ] @transaction.atomic @@ -1954,22 +1980,7 @@ class Meta: pending_instalment = serializers.SerializerMethodField("get_pending_instalment") def get_pending_instalment(self, application): - """Get the latest pending instalment for the application""" - try: - instalments = application.calculation.instalments.filter( - instalment_number__gt=1 - ) - instalment = ( - instalments.exclude(status=InstalmentStatus.COMPLETED) - .order_by("-due_date") - .first() - or None - ) - if instalment is not None: - return InstalmentSerializer(instalment).data - except AttributeError: - return None - return None + return _get_pending_instalment(application, [InstalmentStatus.COMPLETED]) ahjo_error = serializers.SerializerMethodField("get_latest_ahjo_error") diff --git a/backend/benefit/calculator/api/v1/serializers.py b/backend/benefit/calculator/api/v1/serializers.py index 43d5a22790..83692a3948 100644 --- a/backend/benefit/calculator/api/v1/serializers.py +++ b/backend/benefit/calculator/api/v1/serializers.py @@ -71,6 +71,14 @@ class Meta: "modified_at", ] + def validate_status(self, status): + if status not in InstalmentStatus.values: + raise serializers.ValidationError( + {"status": f"status must be one of {InstalmentStatus.values}"}, + ) + + return status + status = serializers.ChoiceField( validators=[InstalmentStatusValidator()], choices=InstalmentStatus.choices, diff --git a/backend/benefit/calculator/api/v1/views.py b/backend/benefit/calculator/api/v1/views.py index 40349805a9..2a6ff9c168 100644 --- a/backend/benefit/calculator/api/v1/views.py +++ b/backend/benefit/calculator/api/v1/views.py @@ -9,6 +9,7 @@ InstalmentSerializer, PreviousBenefitSerializer, ) +from calculator.enums import InstalmentStatus from calculator.models import Instalment, PreviousBenefit from common.permissions import BFIsHandler from shared.audit_log.viewsets import AuditLoggingModelViewSet @@ -46,9 +47,15 @@ class InstalmentView(APIView): def patch(self, request, instalment_id): instalment = get_object_or_404(Instalment, pk=instalment_id) - - serializer = InstalmentSerializer(instalment, data=request.data) + instalment_status = request.data["status"] + serializer = InstalmentSerializer( + instalment, data={"status": instalment_status}, partial=True + ) if serializer.is_valid(): serializer.save() + if instalment_status == InstalmentStatus.CANCELLED: + application = instalment.calculation.application + application.archived = True + application.save() return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) diff --git a/frontend/benefit/handler/public/locales/en/common.json b/frontend/benefit/handler/public/locales/en/common.json index e3358aa033..66affecac7 100644 --- a/frontend/benefit/handler/public/locales/en/common.json +++ b/frontend/benefit/handler/public/locales/en/common.json @@ -224,7 +224,7 @@ "cancelled": "Peruttu", "accepted": "Hyväksytty", "waiting": "Odottaa", - "completed": "Valmis", + "completed": "Maksettu", "error_in_talpa": "Virhe maksussa" }, "calculationEndDate": "Viim. tukipäivä", @@ -831,7 +831,8 @@ "showDecision": "Tarkastele päätöstä", "reportAlteration": "Tee uusi muutosilmoitus" }, - "calculation": "Laskelma" + "calculation": "Laskelma", + "instalments": "Maksuerät" }, "alterations": { "new": { @@ -1373,7 +1374,11 @@ "result": { "header": "Myönnettävä Helsinki-lisä", "header2": "Arvio koostuu seuraavista tiedoista:", - "acceptedBenefit": "Myönnettävä Helsinki-lisä" + "header3": "Maksuerät", + "acceptedBenefit": "Myönnettävä Helsinki-lisä", + "firstInstalment": "Ensimmäinen maksuerä", + "secondInstalment": "Toinen maksuerä", + "total": "Yhteensä" }, "errors": { "trainingCompensation": { @@ -1856,5 +1861,13 @@ "label": "Tila" } } + }, + "instalments": { + "dialog": { + "cancelInstalment": { + "heading": "Maksuerän peruutus", + "text": "Oletko varma, että haluat peruuttaa {{sum}} suuruisen maksuerän hakemukselle {{details}}?" + } + } } } diff --git a/frontend/benefit/handler/public/locales/fi/common.json b/frontend/benefit/handler/public/locales/fi/common.json index a922c39852..c0fc8382a9 100644 --- a/frontend/benefit/handler/public/locales/fi/common.json +++ b/frontend/benefit/handler/public/locales/fi/common.json @@ -224,7 +224,7 @@ "cancelled": "Peruttu", "accepted": "Hyväksytty", "waiting": "Odottaa", - "completed": "Valmis", + "completed": "Maksettu", "error_in_talpa": "Virhe maksussa" }, "calculationEndDate": "Viim. tukipäivä", @@ -831,7 +831,8 @@ "showDecision": "Tarkastele päätöstä", "reportAlteration": "Tee uusi muutosilmoitus" }, - "calculation": "Laskelma" + "calculation": "Laskelma", + "instalments": "Maksuerät" }, "alterations": { "new": { @@ -1373,7 +1374,11 @@ "result": { "header": "Myönnettävä Helsinki-lisä", "header2": "Arvio koostuu seuraavista tiedoista:", - "acceptedBenefit": "Myönnettävä Helsinki-lisä" + "header3": "Maksuerät", + "acceptedBenefit": "Myönnettävä Helsinki-lisä", + "firstInstalment": "Ensimmäinen maksuerä", + "secondInstalment": "Toinen maksuerä", + "total": "Yhteensä" }, "errors": { "trainingCompensation": { @@ -1855,5 +1860,13 @@ "label": "Tila" } } + }, + "instalments": { + "dialog": { + "cancelInstalment": { + "heading": "Maksuerän peruutus", + "text": "Oletko varma, että haluat peruuttaa {{sum}} suuruisen maksuerän hakemukselle {{details}}?" + } + } } } diff --git a/frontend/benefit/handler/public/locales/sv/common.json b/frontend/benefit/handler/public/locales/sv/common.json index e3358aa033..66affecac7 100644 --- a/frontend/benefit/handler/public/locales/sv/common.json +++ b/frontend/benefit/handler/public/locales/sv/common.json @@ -224,7 +224,7 @@ "cancelled": "Peruttu", "accepted": "Hyväksytty", "waiting": "Odottaa", - "completed": "Valmis", + "completed": "Maksettu", "error_in_talpa": "Virhe maksussa" }, "calculationEndDate": "Viim. tukipäivä", @@ -831,7 +831,8 @@ "showDecision": "Tarkastele päätöstä", "reportAlteration": "Tee uusi muutosilmoitus" }, - "calculation": "Laskelma" + "calculation": "Laskelma", + "instalments": "Maksuerät" }, "alterations": { "new": { @@ -1373,7 +1374,11 @@ "result": { "header": "Myönnettävä Helsinki-lisä", "header2": "Arvio koostuu seuraavista tiedoista:", - "acceptedBenefit": "Myönnettävä Helsinki-lisä" + "header3": "Maksuerät", + "acceptedBenefit": "Myönnettävä Helsinki-lisä", + "firstInstalment": "Ensimmäinen maksuerä", + "secondInstalment": "Toinen maksuerä", + "total": "Yhteensä" }, "errors": { "trainingCompensation": { @@ -1856,5 +1861,13 @@ "label": "Tila" } } + }, + "instalments": { + "dialog": { + "cancelInstalment": { + "heading": "Maksuerän peruutus", + "text": "Oletko varma, että haluat peruuttaa {{sum}} suuruisen maksuerän hakemukselle {{details}}?" + } + } } } diff --git a/frontend/benefit/handler/src/components/applicationList/ApplicationList.tsx b/frontend/benefit/handler/src/components/applicationList/ApplicationList.tsx index 4448485fdf..7d5e1ceee8 100644 --- a/frontend/benefit/handler/src/components/applicationList/ApplicationList.tsx +++ b/frontend/benefit/handler/src/components/applicationList/ApplicationList.tsx @@ -8,6 +8,7 @@ import { APPLICATION_STATUSES } from 'benefit-shared/constants'; import { AhjoError, ApplicationListItemData, + Instalment, } from 'benefit-shared/types/application'; import { IconSpeechbubbleText, Table, Tag, Tooltip } from 'hds-react'; import * as React from 'react'; @@ -59,18 +60,16 @@ const buildApplicationUrl = ( const getFirstInstalmentTotalAmount = ( calculatedBenefitAmount: string, - pendingInstalmentAmount?: string + pendingInstalment?: Instalment ): string | JSX.Element => { let firstInstalment = parseInt(calculatedBenefitAmount, 10); - if (pendingInstalmentAmount) { - firstInstalment -= parseInt(pendingInstalmentAmount, 10); + if (pendingInstalment) { + firstInstalment -= parseInt(String(pendingInstalment?.amount), 10); } - return pendingInstalmentAmount ? ( + return pendingInstalment ? ( <> - - {formatFloatToCurrency(firstInstalment, null, 'fi-FI', 0)} - {' '} - / {formatFloatToCurrency(calculatedBenefitAmount, 'EUR', 'fi-FI', 0)} + {formatFloatToCurrency(firstInstalment, null, 'fi-FI', 0)} /{' '} + {formatFloatToCurrency(calculatedBenefitAmount, 'EUR', 'fi-FI', 0)} ) : ( formatFloatToCurrency(firstInstalment, 'EUR', 'fi-FI', 0) @@ -328,6 +327,7 @@ const ApplicationList: React.FC = ({ if (inPayment) { cols.push( { + customSortCompareFunction: sortFinnishDate, headerName: getHeader('decisionDate'), key: 'decisionDate', isSortable: true, @@ -348,7 +348,7 @@ const ApplicationList: React.FC = ({ }: ApplicationListTableTransforms) => getFirstInstalmentTotalAmount( String(calculatedBenefitAmount), - String(pendingInstalment?.amount) || null + pendingInstalment || null ), } ); diff --git a/frontend/benefit/handler/src/components/applicationList/ApplicationListForInstalments.tsx b/frontend/benefit/handler/src/components/applicationList/ApplicationListForInstalments.tsx index bdc7600b05..93eee21b3b 100644 --- a/frontend/benefit/handler/src/components/applicationList/ApplicationListForInstalments.tsx +++ b/frontend/benefit/handler/src/components/applicationList/ApplicationListForInstalments.tsx @@ -9,18 +9,23 @@ import { APPLICATION_STATUSES, INSTALMENT_STATUSES, } from 'benefit-shared/constants'; -import { ApplicationListItemData } from 'benefit-shared/types/application'; +import { + ApplicationListItemData, + Instalment, +} from 'benefit-shared/types/application'; import { Button, IconArrowUndo, IconCheck, IconCross, - IconDocument, Table, Tag, } from 'hds-react'; +import noop from 'lodash/noop'; +import { TFunction } from 'next-i18next'; import * as React from 'react'; import LoadingSkeleton from 'react-loading-skeleton'; +import Modal from 'shared/components/modal/Modal'; import { $Link } from 'shared/components/table/Table.sc'; import { convertToUIDateFormat, @@ -29,6 +34,7 @@ import { import { formatFloatToCurrency } from 'shared/utils/string.utils'; import { useTheme } from 'styled-components'; +import ConfirmModalContent from '../applicationReview/actions/ConfirmModalContent/confirm'; import { $Column, $Wrapper, @@ -64,6 +70,25 @@ const buildApplicationUrl = ( return applicationUrl; }; +export const renderInstalmentTagPerStatus = ( + t: TFunction, + pendingInstalment: Instalment +): JSX.Element => ( + <$TagWrapper + $colors={getInstalmentTagStyleForStatus( + pendingInstalment?.status as INSTALMENT_STATUSES + )} + > + + {t( + `common:applications.list.columns.instalmentStatuses.${ + pendingInstalment?.status as INSTALMENT_STATUSES + }` + )} + + +); + const ApplicationListForInstalments: React.FC = ({ heading, list = [], @@ -72,6 +97,8 @@ const ApplicationListForInstalments: React.FC = ({ const { t, translationsBase, getHeader } = useApplicationList(); const theme = useTheme(); const [selectedRows, setSelectedRows] = React.useState([]); + const [isInstalmentCancelModalShown, setIsInstalmentCancelModalShown] = + React.useState(false); const { mutate: changeInstalmentStatus, isLoading: isLoadingStatusChange } = useInstalmentStatusTransition(); @@ -128,21 +155,8 @@ const ApplicationListForInstalments: React.FC = ({ }, { - transform: ({ pendingInstalment }: ApplicationListTableTransforms) => ( - <$TagWrapper - $colors={getInstalmentTagStyleForStatus( - pendingInstalment?.status as INSTALMENT_STATUSES - )} - > - - {t( - `common:applications.list.columns.instalmentStatuses.${ - pendingInstalment?.status as INSTALMENT_STATUSES - }` - )} - - - ), + transform: ({ pendingInstalment }: ApplicationListTableTransforms) => + renderInstalmentTagPerStatus(t, pendingInstalment), headerName: getHeader('paymentStatus'), key: 'status', isSortable: true, @@ -153,14 +167,7 @@ const ApplicationListForInstalments: React.FC = ({ pendingInstalment, }: ApplicationListTableTransforms) => ( <> - - {formatFloatToCurrency( - pendingInstalment?.amount, - null, - 'fi-FI', - 0 - )}{' '} - + {formatFloatToCurrency(pendingInstalment?.amount, null, 'fi-FI', 0)}{' '} /{' '} {formatFloatToCurrency(calculatedBenefitAmount, 'EUR', 'fi-FI', 0)} @@ -199,6 +206,14 @@ const ApplicationListForInstalments: React.FC = ({ app.id === String(selectedApplication?.id) )?.pendingInstalment || null; + const onSubmitCancel = (): void => { + changeInstalmentStatus({ + id: selectedInstalment?.id, + status: INSTALMENT_STATUSES.CANCELLED, + }); + setIsInstalmentCancelModalShown(false); + }; + return ( <$InstalmentList data-testid="instalment-list"> {list.length > 0 ? ( @@ -249,12 +264,7 @@ const ApplicationListForInstalments: React.FC = ({ disabled={isLoading || isLoadingStatusChange} theme="coat" iconLeft={} - onClick={() => - changeInstalmentStatus({ - id: selectedInstalment.id, - status: INSTALMENT_STATUSES.CANCELLED, - }) - } + onClick={() => setIsInstalmentCancelModalShown(true)} > {t(`${translationsBase}.actions.cancel`)} @@ -280,32 +290,39 @@ const ApplicationListForInstalments: React.FC = ({ {t(`${translationsBase}.actions.return`)} )} - - {[ - INSTALMENT_STATUSES.CANCELLED, - INSTALMENT_STATUSES.PAID, - ].includes( - selectedInstalment?.status as INSTALMENT_STATUSES - ) && ( - - )} )} + setIsInstalmentCancelModalShown(false)} + onSubmit={onSubmitCancel} + /> + } + /> ) : ( <$EmptyHeading> diff --git a/frontend/benefit/handler/src/components/applicationReview/handlingView/DecisionCalculationAccordion.sc.ts b/frontend/benefit/handler/src/components/applicationReview/handlingView/DecisionCalculationAccordion.sc.ts index 8134ad94eb..db1b2df02a 100644 --- a/frontend/benefit/handler/src/components/applicationReview/handlingView/DecisionCalculationAccordion.sc.ts +++ b/frontend/benefit/handler/src/components/applicationReview/handlingView/DecisionCalculationAccordion.sc.ts @@ -4,7 +4,11 @@ import styled from 'styled-components'; export const $DecisionCalculatorAccordion = styled.div` position: relative; - div[role="heading"] > div[role="button"] > span.label { + &:not(:last-child) { + margin-bottom: ${(props) => props.theme.spacing.xs}; + } + + div[role='heading'] > div[role='button'] > span.label { padding-left: ${(props) => props.theme.spacing.xl}; } `; diff --git a/frontend/benefit/handler/src/components/applicationReview/handlingView/DecisionCalculationAccordion.tsx b/frontend/benefit/handler/src/components/applicationReview/handlingView/DecisionCalculationAccordion.tsx index 12a873619f..ab7aac6581 100644 --- a/frontend/benefit/handler/src/components/applicationReview/handlingView/DecisionCalculationAccordion.tsx +++ b/frontend/benefit/handler/src/components/applicationReview/handlingView/DecisionCalculationAccordion.tsx @@ -13,16 +13,20 @@ import { import { CALCULATION_ROW_DESCRIPTION_TYPES, CALCULATION_ROW_TYPES, + INSTALMENT_STATUSES, } from 'benefit-shared/constants'; import { Application } from 'benefit-shared/types/application'; -import { Accordion, IconGlyphEuro } from 'hds-react'; +import { Accordion, IconBagCogwheel, IconGlyphEuro } from 'hds-react'; +import Link from 'next/link'; import { useTranslation } from 'next-i18next'; import * as React from 'react'; import { $ViewField } from 'shared/components/benefit/summaryView/SummaryView.sc'; import { $GridCell } from 'shared/components/forms/section/FormSection.sc'; +import { convertToUIDateFormat } from 'shared/utils/date.utils'; import { formatFloatToCurrency } from 'shared/utils/string.utils'; import { useTheme } from 'styled-components'; +import { renderInstalmentTagPerStatus } from '../../applicationList/ApplicationListForInstalments'; import { $CalculatorTableHeader, $CalculatorTableRow, @@ -43,110 +47,200 @@ const DecisionCalculationAccordion: React.FC = ({ data }) => { const sections = groupCalculatorRows(rowsWithoutTotal); const headingSize = { fontSize: theme.fontSize.heading.l }; + const secondInstalmentText = data.pendingInstalment ? ( + <> + {' '} + {t(`${translationsBase}.secondInstalment`)}{' '} + {convertToUIDateFormat(data.pendingInstalment.dueDate)} + + ) : null; return ( - <$DecisionCalculatorAccordion> - <$DecisionCalculatorAccordionIconContainer aria-hidden="true"> - - - - <$GridCell - $colSpan={11} - style={{ - padding: theme.spacing.m, - }} + <> + <$DecisionCalculatorAccordion> + <$DecisionCalculatorAccordionIconContainer aria-hidden="true"> + + + - <$CalculatorContainer> - {totalRow && ( - <> - <$CalculatorTableHeader css={headingSize}> - {t(`${translationsBase}.header`)} - - -
- - )} - <$CalculatorTableHeader - style={{ paddingBottom: theme.spacing.m }} - css={headingSize} - > - {t(`${translationsBase}.header2`)} - - {sections.map((section) => { - const firstRowIsMonthSubtotal = [ - CALCULATION_ROW_TYPES.HELSINKI_BENEFIT_MONTHLY_EUR, - CALCULATION_ROW_TYPES.HELSINKI_BENEFIT_SUB_TOTAL_EUR, - ].includes(section[0]?.rowType); + <$GridCell + $colSpan={11} + style={{ + padding: theme.spacing.m, + }} + > + <$CalculatorContainer> + {totalRow && ( + <> + <$CalculatorTableHeader css={headingSize}> + {t(`${translationsBase}.header`)} + + +
+ + )} + <$CalculatorTableHeader + style={{ paddingBottom: theme.spacing.m }} + css={headingSize} + > + {t(`${translationsBase}.header2`)} + + {sections.map((section) => { + const firstRowIsMonthSubtotal = [ + CALCULATION_ROW_TYPES.HELSINKI_BENEFIT_MONTHLY_EUR, + CALCULATION_ROW_TYPES.HELSINKI_BENEFIT_SUB_TOTAL_EUR, + ].includes(section[0]?.rowType); - return ( - <$Section - key={section[0].id || 'filler'} - className={firstRowIsMonthSubtotal ? 'subtotal' : ''} - > - {section.map((row) => { - const isDateRange = - CALCULATION_ROW_DESCRIPTION_TYPES.DATE === - row.descriptionType; - const isDescriptionRowType = - CALCULATION_ROW_TYPES.DESCRIPTION === row.rowType; + return ( + <$Section + key={section[0].id || 'filler'} + className={firstRowIsMonthSubtotal ? 'subtotal' : ''} + > + {section.map((row) => { + const isDateRange = + CALCULATION_ROW_DESCRIPTION_TYPES.DATE === + row.descriptionType; + const isDescriptionRowType = + CALCULATION_ROW_TYPES.DESCRIPTION === row.rowType; - const isPerMonth = CALCULATION_PER_MONTH_ROW_TYPES.includes( - row.rowType - ); - return ( -
- {CALCULATION_ROW_TYPES.HELSINKI_BENEFIT_MONTHLY_EUR === - row.rowType && ( - <$CalculatorTableRow> - <$ViewField isBold> - {t(`${translationsBase}.acceptedBenefit`)} - - - )} - <$CalculatorTableRow - isNewSection={isDateRange} - style={{ - marginBottom: '7px', - }} - > - <$ViewField - isBold={isDateRange || isDescriptionRowType} - isBig={isDateRange} + const isPerMonth = + CALCULATION_PER_MONTH_ROW_TYPES.includes(row.rowType); + return ( +
+ {CALCULATION_ROW_TYPES.HELSINKI_BENEFIT_MONTHLY_EUR === + row.rowType && ( + <$CalculatorTableRow> + <$ViewField isBold> + {t(`${translationsBase}.acceptedBenefit`)} + + + )} + <$CalculatorTableRow + isNewSection={isDateRange} + style={{ + marginBottom: '7px', + }} > - {row.descriptionFi} - - {!isDescriptionRowType && ( <$ViewField - isBold - style={{ marginRight: theme.spacing.xl4 }} + isBold={isDateRange || isDescriptionRowType} + isBig={isDateRange} > - {formatFloatToCurrency(row.amount)} - {isPerMonth && t('common:utility.perMonth')} + {row.descriptionFi} - )} - -
- ); - })} + {!isDescriptionRowType && ( + <$ViewField + isBold + style={{ marginRight: theme.spacing.xl4 }} + > + {formatFloatToCurrency(row.amount)} + {isPerMonth && t('common:utility.perMonth')} + + )} + +
+ ); + })} + + ); + })} + + +
+ + {data.pendingInstalment && ( + <$DecisionCalculatorAccordion> + <$DecisionCalculatorAccordionIconContainer aria-hidden="true"> + + + + <$GridCell + $colSpan={11} + style={{ + padding: theme.spacing.m, + }} + > + <$CalculatorContainer> + <$Section className=""> + <$CalculatorTableRow> + <$ViewField> + {t(`${translationsBase}.firstInstalment`)} + + {formatFloatToCurrency( + data.calculatedBenefitAmount - + data.pendingInstalment.amount, + 'EUR', + 'fi-FI', + 0 + )}{' '} + + + <$Section className=""> + <$CalculatorTableRow> + <$ViewField> + {[ + INSTALMENT_STATUSES.WAITING, + INSTALMENT_STATUSES.ERROR_IN_TALPA, + INSTALMENT_STATUSES.CANCELLED, + INSTALMENT_STATUSES.ACCEPTED, + ].includes( + data.pendingInstalment.status as INSTALMENT_STATUSES + ) ? ( + {secondInstalmentText} + ) : ( + secondInstalmentText + )} + +
+ {renderInstalmentTagPerStatus(t, data.pendingInstalment)} + {formatFloatToCurrency( + data.pendingInstalment.amount, + 'EUR', + 'fi-FI', + 0 + )} +
+ + + <$Section className="subtotal"> + <$CalculatorTableRow> + <$ViewField isBold isBig> + {t(`${translationsBase}.total`)} + + {formatFloatToCurrency( + data.calculatedBenefitAmount, + 'EUR', + 'fi-FI', + 0 + )} + - ); - })} - - -
- + + +
+ + )} + ); }; diff --git a/frontend/benefit/shared/src/types/application.d.ts b/frontend/benefit/shared/src/types/application.d.ts index 7227d6f701..663b9d9425 100644 --- a/frontend/benefit/shared/src/types/application.d.ts +++ b/frontend/benefit/shared/src/types/application.d.ts @@ -323,6 +323,7 @@ export type Application = { ahjoStatus?: string; handledByAhjoAutomation?: boolean; batchStatus?: BATCH_STATUSES; + pendingInstalment?: Instalment; } & Step1 & Step2; @@ -419,10 +420,18 @@ export type PaySubsidyData = { duration_in_months_rounded: string; }; +export type InstalmentData = { + id: string; + instalment_number: number; + amount: number; + due_date: string; + status: INSTALMENT_STATUSES; +}; + export type Instalment = { id: string; instalmentNumber: number; - amount: string; + amount: number; dueDate: string; status: INSTALMENT_STATUSES; }; @@ -499,7 +508,7 @@ export type ApplicationData = { handled_by_ahjo_automation?: boolean; alterations: ApplicationAlterationData[]; ahjo_error?: AhjoErrorData; - pending_instalment?: Instalment; + pending_instalment?: InstalmentData; }; export type EmployeeData = {