diff --git a/.yarn/patches/cache-manager-ioredis-yet-npm-1.1.0-da7d1a9865.patch b/.yarn/patches/cache-manager-ioredis-yet-npm-1.1.0-da7d1a9865.patch deleted file mode 100644 index c97e9538ce39..000000000000 --- a/.yarn/patches/cache-manager-ioredis-yet-npm-1.1.0-da7d1a9865.patch +++ /dev/null @@ -1,26 +0,0 @@ -diff --git a/dist/index.js b/dist/index.js -index 21d1ae2f70d7c5b38e17d053a9bf68454edfe98a..6045a01e347041e0cb0b7025dc5734d9f44a9bfb 100644 ---- a/dist/index.js -+++ b/dist/index.js -@@ -90,7 +90,7 @@ function builder(redisCache, reset, keys, options) { - yield redisCache.del(key); - }); - }, -- ttl: (key) => __awaiter(this, void 0, void 0, function* () { return redisCache.ttl(key); }), -+ ttl: (key) => __awaiter(this, void 0, void 0, function* () { return redisCache.pttl(key); }), - keys: (pattern = '*') => keys(pattern), - reset, - isCacheable, -diff --git a/src/index.ts b/src/index.ts -index 267bdf152027f4bc6f0321c186542ce2dc5f3bb1..a295c85899f6206de13d202967e7676247925a53 100644 ---- a/src/index.ts -+++ b/src/index.ts -@@ -87,7 +87,7 @@ function builder( - async del(key) { - await redisCache.del(key); - }, -- ttl: async (key) => redisCache.ttl(key), -+ ttl: async (key) => redisCache.pttl(key), - keys: (pattern = '*') => keys(pattern), - reset, - isCacheable, diff --git a/apps/api/infra/api.ts b/apps/api/infra/api.ts index 7dfa73db5403..792310df3993 100644 --- a/apps/api/infra/api.ts +++ b/apps/api/infra/api.ts @@ -440,8 +440,8 @@ export const serviceSetup = (services: { .readiness('/health') .liveness('/liveness') .resources({ - limits: { cpu: '600m', memory: '2048Mi' }, - requests: { cpu: '250m', memory: '896Mi' }, + limits: { cpu: '1200m', memory: '2048Mi' }, + requests: { cpu: '350m', memory: '896Mi' }, }) .replicaCount({ default: 2, diff --git a/apps/api/src/app/graphql-options.factory.ts b/apps/api/src/app/graphql-options.factory.ts index b85596f9013e..508695058d06 100644 --- a/apps/api/src/app/graphql-options.factory.ts +++ b/apps/api/src/app/graphql-options.factory.ts @@ -23,7 +23,6 @@ export class GraphqlOptionsFactory implements GqlOptionsFactory { ? true : 'apps/api/src/api.graphql' const bypassCacheSecret = this.config.bypassCacheSecret - return { debug, playground, @@ -31,7 +30,7 @@ export class GraphqlOptionsFactory implements GqlOptionsFactory { path: '/api/graphql', cache: this.config.redis.nodes.length > 0 - ? await createRedisApolloCache({ + ? createRedisApolloCache({ name: 'apollo-cache', nodes: this.config.redis.nodes, ssl: this.config.redis.ssl, diff --git a/apps/financial-aid/api/src/app/modules/application/dto/createApplication.input.ts b/apps/financial-aid/api/src/app/modules/application/dto/createApplication.input.ts index 682624b9253c..20f0010688ad 100644 --- a/apps/financial-aid/api/src/app/modules/application/dto/createApplication.input.ts +++ b/apps/financial-aid/api/src/app/modules/application/dto/createApplication.input.ts @@ -79,6 +79,10 @@ export class CreateApplicationInput implements CreateApplication { @Field({ nullable: true }) readonly formComment?: string + @Allow() + @Field({ nullable: true }) + readonly childrenComment?: string + @Allow() @Field(() => String) readonly state!: ApplicationState diff --git a/apps/financial-aid/api/src/app/modules/application/models/application.model.ts b/apps/financial-aid/api/src/app/modules/application/models/application.model.ts index 7c12b9d89ae3..a5c584c9008a 100644 --- a/apps/financial-aid/api/src/app/modules/application/models/application.model.ts +++ b/apps/financial-aid/api/src/app/modules/application/models/application.model.ts @@ -77,6 +77,9 @@ export class ApplicationModel implements Application { @Field({ nullable: true }) readonly formComment?: string + @Field({ nullable: true }) + readonly childrenComment?: string + @Field({ nullable: true }) readonly spouseFormComment?: string diff --git a/apps/financial-aid/backend/migrations/20240522105449-update-application-add-children-comment.js b/apps/financial-aid/backend/migrations/20240522105449-update-application-add-children-comment.js new file mode 100644 index 000000000000..ab2b367b5cea --- /dev/null +++ b/apps/financial-aid/backend/migrations/20240522105449-update-application-add-children-comment.js @@ -0,0 +1,29 @@ +'use strict' + +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.sequelize.transaction((t) => + Promise.all([ + queryInterface.addColumn( + 'applications', + 'children_comment', + { + type: Sequelize.TEXT, + allowNull: true, + }, + { transaction: t }, + ), + ]), + ) + }, + + down: (queryInterface, Sequelize) => { + return queryInterface.sequelize.transaction((t) => + Promise.all([ + queryInterface.removeColumn('applications', 'children_comment', { + transaction: t, + }), + ]), + ) + }, +} diff --git a/apps/financial-aid/backend/src/app/modules/application/application.service.ts b/apps/financial-aid/backend/src/app/modules/application/application.service.ts index ec07735f48e4..c12edc29ee92 100644 --- a/apps/financial-aid/backend/src/app/modules/application/application.service.ts +++ b/apps/financial-aid/backend/src/app/modules/application/application.service.ts @@ -507,6 +507,12 @@ export class ApplicationService { updatedApplication?.setDataValue('applicationEvents', eventsResolved) }) + const children = this.childrenService + .findById(id) + .then((childrenResolved) => { + updatedApplication?.setDataValue('children', childrenResolved) + }) + const files = this.fileService .getAllApplicationFiles(id) .then((filesResolved) => { @@ -534,7 +540,7 @@ export class ApplicationService { ]) } - await Promise.all([events, files, directTaxPayments]) + await Promise.all([events, files, directTaxPayments, children]) return updatedApplication } diff --git a/apps/financial-aid/backend/src/app/modules/application/dto/createApplication.dto.ts b/apps/financial-aid/backend/src/app/modules/application/dto/createApplication.dto.ts index 7ef2dd15e797..5bb5d2364090 100644 --- a/apps/financial-aid/backend/src/app/modules/application/dto/createApplication.dto.ts +++ b/apps/financial-aid/backend/src/app/modules/application/dto/createApplication.dto.ts @@ -106,6 +106,11 @@ export class CreateApplicationDto { @ApiProperty() readonly formComment: string + @IsOptional() + @IsString() + @ApiProperty() + readonly childrenComment: string + @IsNotEmpty() @IsString() @ApiProperty() diff --git a/apps/financial-aid/backend/src/app/modules/application/models/application.model.ts b/apps/financial-aid/backend/src/app/modules/application/models/application.model.ts index 13af877e2b6b..049dd7656769 100644 --- a/apps/financial-aid/backend/src/app/modules/application/models/application.model.ts +++ b/apps/financial-aid/backend/src/app/modules/application/models/application.model.ts @@ -164,12 +164,19 @@ export class ApplicationModel extends Model { interview: boolean @Column({ - type: DataType.STRING, + type: DataType.TEXT, allowNull: true, }) @ApiProperty() formComment: string + @Column({ + type: DataType.TEXT, + allowNull: true, + }) + @ApiProperty() + childrenComment: string + @Column({ type: DataType.STRING, allowNull: true, diff --git a/apps/financial-aid/backend/src/app/modules/application/test/update.spec.ts b/apps/financial-aid/backend/src/app/modules/application/test/update.spec.ts index 934f945e89c5..b39f4d3679d4 100644 --- a/apps/financial-aid/backend/src/app/modules/application/test/update.spec.ts +++ b/apps/financial-aid/backend/src/app/modules/application/test/update.spec.ts @@ -21,6 +21,7 @@ import { createTestingApplicationModule } from './createTestingApplicationModule import type { User } from '@island.is/auth-nest-tools' import { MunicipalitiesFinancialAidScope } from '@island.is/auth/scopes' import { DirectTaxPaymentService } from '../../directTaxPayment' +import { ChildrenService } from '../../children' interface Then { result: ApplicationModel @@ -45,6 +46,7 @@ describe('ApplicationController - Update', () => { let mockMunicipalityService: MunicipalityService let mockEmailService: EmailService let mockDirectTaxPaymentService: DirectTaxPaymentService + let mockChildrenService: ChildrenService beforeEach(async () => { const { @@ -57,6 +59,7 @@ describe('ApplicationController - Update', () => { municipalityService, emailService, directTaxPaymentService, + childrenService, } = await createTestingApplicationModule() mockApplicationModel = applicationModel @@ -67,6 +70,7 @@ describe('ApplicationController - Update', () => { mockMunicipalityService = municipalityService mockEmailService = emailService mockDirectTaxPaymentService = directTaxPaymentService + mockChildrenService = childrenService givenWhenThen = async ( id: string, @@ -158,6 +162,8 @@ describe('ApplicationController - Update', () => { const getDirectTaxPayment = mockDirectTaxPaymentService.getByApplicationId as jest.Mock getDirectTaxPayment.mockReturnValueOnce(Promise.resolve([])) + const getChildren = mockChildrenService.findById as jest.Mock + getChildren.mockReturnValueOnce(Promise.resolve([])) then = await givenWhenThen(id, applicationUpdate, user) }) @@ -273,6 +279,8 @@ describe('ApplicationController - Update', () => { const getDirectTaxPayment = mockDirectTaxPaymentService.getByApplicationId as jest.Mock getDirectTaxPayment.mockReturnValueOnce(Promise.resolve([])) + const getChildren = mockChildrenService.findById as jest.Mock + getChildren.mockReturnValueOnce(Promise.resolve([])) }) describe('Allowed events', () => { @@ -382,6 +390,8 @@ describe('ApplicationController - Update', () => { const getDirectTaxPayment = mockDirectTaxPaymentService.getByApplicationId as jest.Mock getDirectTaxPayment.mockReturnValueOnce(Promise.resolve([])) + const getChildren = mockChildrenService.findById as jest.Mock + getChildren.mockReturnValueOnce(Promise.resolve([])) }) describe('Forbidden events', () => { @@ -498,6 +508,8 @@ describe('ApplicationController - Update', () => { const getDirectTaxPayment = mockDirectTaxPaymentService.getByApplicationId as jest.Mock getDirectTaxPayment.mockReturnValueOnce(Promise.resolve([])) + const getChildren = mockChildrenService.findById as jest.Mock + getChildren.mockReturnValueOnce(Promise.resolve([])) then = await givenWhenThen(id, applicationUpdate, staff) }) diff --git a/apps/financial-aid/web-veita/graphql/sharedGql.ts b/apps/financial-aid/web-veita/graphql/sharedGql.ts index fd3725ecb5d2..d00a9a67edfb 100644 --- a/apps/financial-aid/web-veita/graphql/sharedGql.ts +++ b/apps/financial-aid/web-veita/graphql/sharedGql.ts @@ -40,6 +40,7 @@ export const ApplicationQuery = gql` } state formComment + childrenComment spouseFormComment municipalityCode studentCustom @@ -200,6 +201,7 @@ export const ApplicationEventMutation = gql` mutation CreateApplicationEvent($input: CreateApplicationEventInput!) { createApplicationEvent(input: $input) { id + applicationSystemId nationalId created modified @@ -217,6 +219,14 @@ export const ApplicationEventMutation = gql` interview employmentCustom homeCircumstancesCustom + familyStatus + spouseNationalId + spouseName + spouseEmail + spousePhoneNumber + city + streetName + postalCode files { id applicationId @@ -227,11 +237,15 @@ export const ApplicationEventMutation = gql` } state formComment + childrenComment + spouseFormComment + municipalityCode studentCustom rejection staff { name municipalityIds + nationalId } applicationEvents { id @@ -239,8 +253,28 @@ export const ApplicationEventMutation = gql` eventType comment created - staffNationalId staffName + staffNationalId + emailSent + } + children { + id + applicationId + nationalId + name + school + } + amount { + aidAmount + income + personalTaxCredit + spousePersonalTaxCredit + tax + finalAmount + deductionFactors { + description + amount + } } spouseHasFetchedDirectTaxPayment hasFetchedDirectTaxPayment @@ -284,6 +318,7 @@ export const UpdateApplicationMutation = gql` mutation UpdateApplicationMutation($input: UpdateApplicationInput!) { updateApplication(input: $input) { id + applicationSystemId nationalId created modified @@ -306,7 +341,6 @@ export const UpdateApplicationMutation = gql` spouseName spouseEmail spousePhoneNumber - municipalityCode city streetName postalCode @@ -320,9 +354,16 @@ export const UpdateApplicationMutation = gql` } state formComment + childrenComment spouseFormComment + municipalityCode studentCustom rejection + staff { + name + municipalityIds + nationalId + } applicationEvents { id applicationId @@ -340,22 +381,6 @@ export const UpdateApplicationMutation = gql` name school } - staff { - name - municipalityIds - nationalId - } - spouseHasFetchedDirectTaxPayment - hasFetchedDirectTaxPayment - directTaxPayments { - totalSalary - payerNationalId - personalAllowance - withheldAtSource - month - year - userType - } amount { aidAmount income @@ -368,6 +393,17 @@ export const UpdateApplicationMutation = gql` amount } } + spouseHasFetchedDirectTaxPayment + hasFetchedDirectTaxPayment + directTaxPayments { + totalSalary + payerNationalId + personalAllowance + withheldAtSource + month + year + userType + } navSuccess } } diff --git a/apps/financial-aid/web-veita/src/utils/applicationHelper.ts b/apps/financial-aid/web-veita/src/utils/applicationHelper.ts index 4c946b400845..4f59bca9ed80 100644 --- a/apps/financial-aid/web-veita/src/utils/applicationHelper.ts +++ b/apps/financial-aid/web-veita/src/utils/applicationHelper.ts @@ -143,7 +143,18 @@ export const getChildrenInfo = (application: Application) => { ] }) - return allChildren.flat() + const childrenComment = application.childrenComment + ? [ + { + title: 'Athugasemd', + content: '', + other: application.childrenComment, + fullWidth: commentFullWidth(application.childrenComment), + }, + ] + : [] + + return allChildren.flat().concat(childrenComment) } export const getApplicantSpouse = (application: Application) => { diff --git a/apps/judicial-system/api/src/app/modules/defendant/dto/updateDefendant.input.ts b/apps/judicial-system/api/src/app/modules/defendant/dto/updateDefendant.input.ts index 376ade4c40d1..8b179cee53a5 100644 --- a/apps/judicial-system/api/src/app/modules/defendant/dto/updateDefendant.input.ts +++ b/apps/judicial-system/api/src/app/modules/defendant/dto/updateDefendant.input.ts @@ -82,4 +82,9 @@ export class UpdateDefendantInput { @IsOptional() @Field(() => ServiceRequirement, { nullable: true }) readonly serviceRequirement?: ServiceRequirement + + @Allow() + @IsOptional() + @Field(() => String, { nullable: true }) + readonly verdictViewDate?: string } diff --git a/apps/judicial-system/api/src/app/modules/defendant/models/defendant.model.ts b/apps/judicial-system/api/src/app/modules/defendant/models/defendant.model.ts index b2e8e5981883..be22c0f5a29c 100644 --- a/apps/judicial-system/api/src/app/modules/defendant/models/defendant.model.ts +++ b/apps/judicial-system/api/src/app/modules/defendant/models/defendant.model.ts @@ -62,4 +62,7 @@ export class Defendant { @Field(() => ServiceRequirement, { nullable: true }) readonly serviceRequirement?: ServiceRequirement + + @Field(() => String, { nullable: true }) + readonly verdictViewDate?: string } diff --git a/apps/judicial-system/backend/migrations/20240514111505-update-defendant.js b/apps/judicial-system/backend/migrations/20240514111505-update-defendant.js new file mode 100644 index 000000000000..d9be034fbb91 --- /dev/null +++ b/apps/judicial-system/backend/migrations/20240514111505-update-defendant.js @@ -0,0 +1,22 @@ +'use strict' + +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.sequelize.transaction((t) => + queryInterface.addColumn( + 'defendant', + 'verdict_view_date', + { type: Sequelize.DATE, allowNull: true }, + { transaction: t }, + ), + ) + }, + + down: (queryInterface) => { + return queryInterface.sequelize.transaction((t) => + queryInterface.removeColumn('defendant', 'verdict_view_date', { + transaction: t, + }), + ) + }, +} diff --git a/apps/judicial-system/backend/src/app/modules/defendant/defendant.controller.ts b/apps/judicial-system/backend/src/app/modules/defendant/defendant.controller.ts index 71922368ffe9..c7f86416e0f0 100644 --- a/apps/judicial-system/backend/src/app/modules/defendant/defendant.controller.ts +++ b/apps/judicial-system/backend/src/app/modules/defendant/defendant.controller.ts @@ -27,6 +27,7 @@ import { districtCourtRegistrarRule, prosecutorRepresentativeRule, prosecutorRule, + publicProsecutorStaffRule, } from '../../guards' import { Case, CaseExistsGuard, CaseWriteGuard, CurrentCase } from '../case' import { CreateDefendantDto } from './dto/createDefendant.dto' @@ -71,6 +72,7 @@ export class DefendantController { districtCourtJudgeRule, districtCourtRegistrarRule, districtCourtAssistantRule, + publicProsecutorStaffRule, ) @Patch(':defendantId') @ApiOkResponse({ diff --git a/apps/judicial-system/backend/src/app/modules/defendant/dto/updateDefendant.dto.ts b/apps/judicial-system/backend/src/app/modules/defendant/dto/updateDefendant.dto.ts index 3de0c18afb4e..922535aab96e 100644 --- a/apps/judicial-system/backend/src/app/modules/defendant/dto/updateDefendant.dto.ts +++ b/apps/judicial-system/backend/src/app/modules/defendant/dto/updateDefendant.dto.ts @@ -73,4 +73,9 @@ export class UpdateDefendantDto { @IsEnum(ServiceRequirement) @ApiPropertyOptional({ enum: ServiceRequirement }) readonly serviceRequirement?: ServiceRequirement + + @IsOptional() + @IsString() + @ApiPropertyOptional() + readonly verdictViewDate?: string } diff --git a/apps/judicial-system/backend/src/app/modules/defendant/models/defendant.model.ts b/apps/judicial-system/backend/src/app/modules/defendant/models/defendant.model.ts index bc3f2e426cef..053d19ff4bf2 100644 --- a/apps/judicial-system/backend/src/app/modules/defendant/models/defendant.model.ts +++ b/apps/judicial-system/backend/src/app/modules/defendant/models/defendant.model.ts @@ -113,4 +113,11 @@ export class Defendant extends Model { }) @ApiProperty({ enum: ServiceRequirement }) serviceRequirement?: ServiceRequirement + + @Column({ + type: DataType.STRING, + allowNull: true, + }) + @ApiProperty() + verdictViewDate?: string } diff --git a/apps/judicial-system/backend/src/app/modules/defendant/test/defendantController/updateRolesRules.spec.ts b/apps/judicial-system/backend/src/app/modules/defendant/test/defendantController/updateRolesRules.spec.ts index 3daff84becec..4cf550c25287 100644 --- a/apps/judicial-system/backend/src/app/modules/defendant/test/defendantController/updateRolesRules.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/defendant/test/defendantController/updateRolesRules.spec.ts @@ -4,6 +4,7 @@ import { districtCourtRegistrarRule, prosecutorRepresentativeRule, prosecutorRule, + publicProsecutorStaffRule, } from '../../../../guards' import { DefendantController } from '../../defendant.controller' @@ -19,11 +20,12 @@ describe('DefendantController - Update rules', () => { }) it('should give permission to roles', () => { - expect(rules).toHaveLength(5) + expect(rules).toHaveLength(6) expect(rules).toContain(prosecutorRule) expect(rules).toContain(prosecutorRepresentativeRule) expect(rules).toContain(districtCourtJudgeRule) expect(rules).toContain(districtCourtRegistrarRule) expect(rules).toContain(districtCourtAssistantRule) + expect(rules).toContain(publicProsecutorStaffRule) }) }) diff --git a/apps/judicial-system/web/src/components/FormProvider/case.graphql b/apps/judicial-system/web/src/components/FormProvider/case.graphql index ffd4abefce09..dd224b72299b 100644 --- a/apps/judicial-system/web/src/components/FormProvider/case.graphql +++ b/apps/judicial-system/web/src/components/FormProvider/case.graphql @@ -23,6 +23,7 @@ query Case($input: CaseQueryInput!) { defendantWaivesRightToCounsel defendantPlea serviceRequirement + verdictViewDate } defenderName defenderNationalId diff --git a/apps/judicial-system/web/src/components/FormProvider/limitedAccessCase.graphql b/apps/judicial-system/web/src/components/FormProvider/limitedAccessCase.graphql index df1d08288313..e54f9fba060c 100644 --- a/apps/judicial-system/web/src/components/FormProvider/limitedAccessCase.graphql +++ b/apps/judicial-system/web/src/components/FormProvider/limitedAccessCase.graphql @@ -28,6 +28,7 @@ query LimitedAccessCase($input: CaseQueryInput!) { defenderEmail defenderPhoneNumber defendantWaivesRightToCounsel + verdictViewDate } defenderName defenderNationalId diff --git a/apps/judicial-system/web/src/components/InfoCard/DefendantInfo/DefendantInfo.css.ts b/apps/judicial-system/web/src/components/InfoCard/DefendantInfo/DefendantInfo.css.ts new file mode 100644 index 000000000000..08d63afd7637 --- /dev/null +++ b/apps/judicial-system/web/src/components/InfoCard/DefendantInfo/DefendantInfo.css.ts @@ -0,0 +1,25 @@ +import { style, styleVariants } from '@vanilla-extract/css' + +import { theme } from '@island.is/island-ui/theme' + +const baseGridRow = style({ + display: 'grid', + gridGap: theme.spacing[1], + marginBottom: theme.spacing[2], +}) + +export const gridRow = styleVariants({ + withButton: [baseGridRow, { gridTemplateColumns: '5fr 1fr' }], + withoutButton: [baseGridRow, { gridTemplateColumns: '1fr' }], +}) + +export const infoCardDefendant = style({ + display: 'flex', + flexDirection: 'column', + + '@media': { + [`screen and (min-width: ${theme.breakpoints.lg}px)`]: { + display: 'block', + }, + }, +}) diff --git a/apps/judicial-system/web/src/components/InfoCard/DefendantInfo/DefendantInfo.strings.ts b/apps/judicial-system/web/src/components/InfoCard/DefendantInfo/DefendantInfo.strings.ts new file mode 100644 index 000000000000..f89786a9be07 --- /dev/null +++ b/apps/judicial-system/web/src/components/InfoCard/DefendantInfo/DefendantInfo.strings.ts @@ -0,0 +1,26 @@ +import { defineMessages } from 'react-intl' + +export const strings = defineMessages({ + appealExpirationDate: { + id: 'judicial.system.core:info_card.defendant_info.appeal_expiration_date', + defaultMessage: 'Áfrýjunarfrestur dómfellda er til {appealExpirationDate}', + description: 'Notað til að birta áfrýjunarfrest dómfellda í ákæru.', + }, + appealDateExpired: { + id: 'judicial.system.core:info_card.defendant_info.appeal_date_expired', + defaultMessage: 'Áfrýjunarfrestur dómfellda var til {appealExpirationDate}', + description: + 'Notað til að láta vita að áfrýjunarfrestur í ákæru er útrunninn.', + }, + appealDateNotBegun: { + id: 'judicial.system.core:info_card.defendant_info.appeal_date_not_begun', + defaultMessage: 'Áfrýjunarfrestur dómfellda er ekki hafinn', + description: + 'Notaður til að láta vita að áfrýjunarfrestur dómfellda er ekki hafinn.', + }, + defender: { + id: 'judicial.system.core:info_card.defendant_info.defender', + defaultMessage: 'Verjandi', + description: 'Notað til að birta titil á verjanda í ákæru.', + }, +}) diff --git a/apps/judicial-system/web/src/components/InfoCard/DefendantInfo/DefendantInfo.tsx b/apps/judicial-system/web/src/components/InfoCard/DefendantInfo/DefendantInfo.tsx new file mode 100644 index 000000000000..8d5b7e4ea0a2 --- /dev/null +++ b/apps/judicial-system/web/src/components/InfoCard/DefendantInfo/DefendantInfo.tsx @@ -0,0 +1,126 @@ +import React, { FC, PropsWithChildren } from 'react' +import { useIntl } from 'react-intl' + +import { + Box, + Button, + IconMapIcon, + LinkV2, + Text, +} from '@island.is/island-ui/core' +import { formatDate, formatDOB } from '@island.is/judicial-system/formatters' +import { Defendant } from '@island.is/judicial-system-web/src/graphql/schema' + +import { strings } from './DefendantInfo.strings' +import { link } from '../../MarkdownWrapper/MarkdownWrapper.css' +import * as styles from './DefendantInfo.css' + +export type DefendantInfoActionButton = { + text: string + onClick: (defendant: Defendant) => void + icon?: IconMapIcon + isDisabled: (defendant: Defendant) => boolean +} + +interface DefendantInfoProps { + defendant: Defendant + displayDefenderInfo: boolean + defendantInfoActionButton?: DefendantInfoActionButton +} + +export const DefendantInfo: FC> = ( + props, +) => { + const { defendant, displayDefenderInfo, defendantInfoActionButton } = props + const { formatMessage } = useIntl() + + const getAppealExpirationInfo = (viewDate?: string) => { + if (!viewDate) { + return formatMessage(strings.appealDateNotBegun) + } + + const today = new Date() + const expiryDate = new Date(viewDate) + expiryDate.setDate(expiryDate.getDate() + 28) + + const message = + today < expiryDate + ? strings.appealExpirationDate + : strings.appealDateExpired + + return formatMessage(message, { + appealExpirationDate: formatDate(expiryDate, 'P'), + }) + } + + return ( +
+
+ + + {defendant.name} + {defendant.nationalId && + `, ${formatDOB(defendant.nationalId, defendant.noNationalId)}`} + + + {defendant.citizenship && `, (${defendant.citizenship})`} + {defendant.address && `, ${defendant.address}`} + + + +
+ + {getAppealExpirationInfo(defendant.verdictViewDate ?? '')} + +
+ + {defendant.defenderName && displayDefenderInfo && ( + + {`${formatMessage(strings.defender)}: ${ + defendant.defenderName + }`} + {defendant.defenderEmail && ( + <> + {`, `} + + {defendant.defenderEmail} + + + )} + + )} +
+ + {defendantInfoActionButton && ( + + + + )} +
+ ) +} diff --git a/apps/judicial-system/web/src/components/InfoCard/InfoCard.css.ts b/apps/judicial-system/web/src/components/InfoCard/InfoCard.css.ts index 4934805dfc9a..e06105c24bec 100644 --- a/apps/judicial-system/web/src/components/InfoCard/InfoCard.css.ts +++ b/apps/judicial-system/web/src/components/InfoCard/InfoCard.css.ts @@ -35,14 +35,3 @@ export const infoCardAdditionalSectionContainer = style({ }, }, }) - -export const infoCardDefendant = style({ - display: 'flex', - flexDirection: 'column', - - '@media': { - [`screen and (min-width: ${theme.breakpoints.lg}px)`]: { - display: 'block', - }, - }, -}) diff --git a/apps/judicial-system/web/src/components/InfoCard/InfoCard.tsx b/apps/judicial-system/web/src/components/InfoCard/InfoCard.tsx index 60c646643ccf..0dd85e15c729 100644 --- a/apps/judicial-system/web/src/components/InfoCard/InfoCard.tsx +++ b/apps/judicial-system/web/src/components/InfoCard/InfoCard.tsx @@ -2,12 +2,15 @@ import React from 'react' import { useIntl } from 'react-intl' import { Box, Icon, IconMapIcon, LinkV2, Text } from '@island.is/island-ui/core' -import { formatDOB } from '@island.is/judicial-system/formatters' import { Defendant, SessionArrangements, } from '@island.is/judicial-system-web/src/graphql/schema' +import { + DefendantInfo, + DefendantInfoActionButton, +} from './DefendantInfo/DefendantInfo' import { strings } from './InfoCard.strings' import { link } from '../MarkdownWrapper/MarkdownWrapper.css' import * as styles from './InfoCard.css' @@ -31,7 +34,11 @@ interface DataSection { interface Props { courtOfAppealData?: Array<{ title: string; value?: React.ReactNode }> data: Array<{ title: string; value?: React.ReactNode }> - defendants?: { title: string; items: Defendant[] } + defendants?: { + title: string + items: Defendant[] + defendantInfoActionButton?: DefendantInfoActionButton + } defenders?: Defender[] icon?: IconMapIcon additionalDataSections?: DataSection[] @@ -108,35 +115,20 @@ const InfoCard: React.FC = (props) => { {defendants && ( <> {defendants.title} - + {defendants.items.map((defendant) => ( - - - {`${defendant.name}, `} - - {defendant.nationalId - ? `${formatDOB( - defendant.nationalId, - defendant.noNationalId, - )}, ` - : ''} - - - {defendant.citizenship && ` (${defendant.citizenship}), `} - - {defendant.address && ( - {`${defendant.address}`} - )} - - + ))} diff --git a/apps/judicial-system/web/src/components/InfoCard/InfoCardClosedIndictment.tsx b/apps/judicial-system/web/src/components/InfoCard/InfoCardClosedIndictment.tsx index f6e2a4ceb7cc..5ef6e72d749a 100644 --- a/apps/judicial-system/web/src/components/InfoCard/InfoCardClosedIndictment.tsx +++ b/apps/judicial-system/web/src/components/InfoCard/InfoCardClosedIndictment.tsx @@ -9,23 +9,19 @@ import { import { core } from '@island.is/judicial-system-web/messages' import { FormContext } from '../FormProvider/FormProvider' +import { DefendantInfoActionButton } from './DefendantInfo/DefendantInfo' import InfoCard, { NameAndEmail } from './InfoCard' import { strings } from './InfoCardIndictment.strings' -const InfoCardClosedIndictment: React.FC< - React.PropsWithChildren -> = () => { +export interface Props { + defendantInfoActionButton?: DefendantInfoActionButton +} + +const InfoCardClosedIndictment: React.FC = (props) => { const { workingCase } = useContext(FormContext) const { formatMessage } = useIntl() - const defenders = workingCase.defendants?.map((defendant) => { - return { - name: defendant.defenderName || '', - defenderNationalId: defendant.defenderNationalId || '', - sessionArrangement: undefined, - email: defendant.defenderEmail || '', - phoneNumber: defendant.defenderPhoneNumber || '', - } - }) + + const { defendantInfoActionButton } = props return ( 1 - ? formatMessage(core.indictmentDefendants) - : formatMessage(core.indictmentDefendant, { + ? formatMessage(strings.indictmentDefendants) + : formatMessage(strings.indictmentDefendant, { gender: workingCase.defendants[0].gender, }), ), items: workingCase.defendants, + defendantInfoActionButton: defendantInfoActionButton, } : undefined } - defenders={defenders} additionalDataSections={[ ...(workingCase.indictmentReviewer?.name ? [ diff --git a/apps/judicial-system/web/src/components/InfoCard/InfoCardIndictment.strings.ts b/apps/judicial-system/web/src/components/InfoCard/InfoCardIndictment.strings.ts index 85316ab91f90..8043cf5d8f8b 100644 --- a/apps/judicial-system/web/src/components/InfoCard/InfoCardIndictment.strings.ts +++ b/apps/judicial-system/web/src/components/InfoCard/InfoCardIndictment.strings.ts @@ -22,4 +22,14 @@ export const strings = defineMessages({ defaultMessage: 'Yfirlestur', description: 'Notaður sem titill á "yfirlestur" hluta af yfirliti ákæru.', }, + indictmentDefendant: { + id: 'judicial.system.core:info_card_indictment.indictment_defendant', + defaultMessage: 'Dómfelldi', + description: 'Notaður sem titill á "dómfelldi" hluta af yfirliti ákæru.', + }, + indictmentDefendants: { + id: 'judicial.system.core:info_card_indictment.indictment_defendants', + defaultMessage: 'Dómfelldu', + description: 'Notaður sem titill á "dómfelldu" hluta af yfirliti ákæru.', + }, }) diff --git a/apps/judicial-system/web/src/routes/PublicProsecutor/Indictments/Overview/Overview.strings.ts b/apps/judicial-system/web/src/routes/PublicProsecutor/Indictments/Overview/Overview.strings.ts index bda2b5b7b75f..bc0a5d574c5f 100644 --- a/apps/judicial-system/web/src/routes/PublicProsecutor/Indictments/Overview/Overview.strings.ts +++ b/apps/judicial-system/web/src/routes/PublicProsecutor/Indictments/Overview/Overview.strings.ts @@ -38,4 +38,25 @@ export const strings = defineMessages({ 'Máli {caseNumber} hefur verið úthlutað til yfirlestrar á {reviewer}.', description: 'Notaður sem texti í tilkynningaglugga um yfirlesara.', }, + displayVerdict: { + id: 'judicial.system.core:public_prosecutor.indictments.overview.display_verdict', + defaultMessage: 'Dómur birtur', + description: 'Notaður sem texti á takka til að birta dóm.', + }, + defendantViewsVerdictModalTitle: { + id: 'judicial.system.core:public_prosecutor.indictments.overview.defendant_views_verdict_modal_title', + defaultMessage: 'Hefur dómur verið birtur dómfellda?', + description: 'Notaður sem titill á tilkynningaglugga um birtan dóm.', + }, + defendantViewsVerdictModalText: { + id: 'judicial.system.core:public_prosecutor.indictments.overview.defendant_views_verdict_modal_text', + defaultMessage: + 'Dómfelldi fær fjögurra vikna frest til að áfrýja dómi eftir að birting hans hefur verið staðfest.', + description: 'Notaður sem texti í tilkynningaglugga um birtan dóm.', + }, + defendantViewsVerdictModalPrimaryButtonText: { + id: 'judicial.system.core:public_prosecutor.indictments.overview.defendant_views_verdict_modal_primary_button_text', + defaultMessage: 'Staðfesta', + description: 'Notaður sem texti á takka til að birta dóm.', + }, }) diff --git a/apps/judicial-system/web/src/routes/PublicProsecutor/Indictments/Overview/Overview.tsx b/apps/judicial-system/web/src/routes/PublicProsecutor/Indictments/Overview/Overview.tsx index c8447d6ea830..eed2faf3d792 100644 --- a/apps/judicial-system/web/src/routes/PublicProsecutor/Indictments/Overview/Overview.tsx +++ b/apps/judicial-system/web/src/routes/PublicProsecutor/Indictments/Overview/Overview.tsx @@ -1,16 +1,11 @@ -import React, { - useCallback, - useContext, - useEffect, - useMemo, - useState, -} from 'react' +import React, { useCallback, useContext, useMemo, useState } from 'react' import { useIntl } from 'react-intl' import { useRouter } from 'next/router' import { Box, Option, Select, Text } from '@island.is/island-ui/core' import * as constants from '@island.is/judicial-system/consts' import { formatDate } from '@island.is/judicial-system/formatters' +import { isCompletedCase } from '@island.is/judicial-system/types' import { core, titles } from '@island.is/judicial-system-web/messages' import { BlueBox, @@ -30,31 +25,32 @@ import { UserContext, } from '@island.is/judicial-system-web/src/components' import { useProsecutorSelectionUsersQuery } from '@island.is/judicial-system-web/src/components/ProsecutorSelection/prosecutorSelectionUsers.generated' -import { useCase } from '@island.is/judicial-system-web/src/utils/hooks' +import { Defendant } from '@island.is/judicial-system-web/src/graphql/schema' +import { + formatDateForServer, + useCase, + useDefendants, +} from '@island.is/judicial-system-web/src/utils/hooks' import { strings } from './Overview.strings' +type VisibleModal = 'REVIEWER_ASSIGNED' | 'DEFENDANT_VIEWS_VERDICT' export const Overview = () => { const router = useRouter() - const { workingCase, isLoadingWorkingCase, caseNotFound } = - useContext(FormContext) const { formatMessage: fm } = useIntl() const { user } = useContext(UserContext) const { updateCase } = useCase() + const { workingCase, isLoadingWorkingCase, caseNotFound, setWorkingCase } = + useContext(FormContext) const [selectedIndictmentReviewer, setSelectedIndictmentReviewer] = useState | null>() - const [modalVisible, setModalVisible] = useState(false) - - useEffect(() => { - setSelectedIndictmentReviewer( - workingCase.indictmentReviewer?.id - ? { - label: workingCase.indictmentReviewer?.name ?? '', - value: workingCase.indictmentReviewer?.id, - } - : null, - ) - }, [workingCase.id, workingCase.indictmentReviewer]) + const [modalVisible, setModalVisible] = useState() + const lawsBroken = useIndictmentsLawsBroken(workingCase) + + const displayReviewerChoices = workingCase.indictmentReviewer === null + + const [selectedDefendant, setSelectedDefendant] = useState() + const { setAndSendDefendantToServer } = useDefendants() const assignReviewer = async () => { if (!selectedIndictmentReviewer) { @@ -67,10 +63,24 @@ export const Overview = () => { return } - setModalVisible(true) + setModalVisible('REVIEWER_ASSIGNED') } - const lawsBroken = useIndictmentsLawsBroken(workingCase) + const handleDefendantViewsVerdict = () => { + if (!selectedDefendant) { + return + } + + const updatedDefendant = { + caseId: workingCase.id, + defendantId: selectedDefendant.id, + verdictViewDate: formatDateForServer(new Date()), + } + + setAndSendDefendantToServer(updatedDefendant, setWorkingCase) + + setModalVisible(undefined) + } const handleNavigationTo = useCallback( (destination: string) => router.push(`${destination}/${workingCase.id}`), @@ -117,7 +127,23 @@ export const Overview = () => { {fm(strings.title)} - + { + setSelectedDefendant(defendant) + setModalVisible('DEFENDANT_VIEWS_VERDICT') + }, + icon: 'mailOpen', + isDisabled: (defendant) => + defendant.verdictViewDate !== null, + } + : undefined + } + /> {lawsBroken.size > 0 && ( @@ -129,49 +155,53 @@ export const Overview = () => { )} - - - {fm(strings.reviewerSubtitle, { - indictmentAppealDeadline: formatDate( - workingCase.indictmentAppealDeadline, - 'P', - ), - })} - - } - /> - - { + setSelectedIndictmentReviewer(value as Option) + }} + isDisabled={loading} + required + /> + + + )} - - - + {displayReviewerChoices && ( + + + + )} - {modalVisible && ( + {modalVisible === 'REVIEWER_ASSIGNED' && ( { onSecondaryButtonClick={() => router.push(constants.CASES_ROUTE)} /> )} + + {modalVisible === 'DEFENDANT_VIEWS_VERDICT' && ( + handleDefendantViewsVerdict()} + secondaryButtonText={fm(core.back)} + onSecondaryButtonClick={() => setModalVisible(undefined)} + /> + )} ) } diff --git a/apps/judicial-system/web/src/routes/PublicProsecutor/Indictments/ReviewDecision/ReviewDecision.css.ts b/apps/judicial-system/web/src/routes/PublicProsecutor/components/ReviewDecision/ReviewDecision.css.ts similarity index 100% rename from apps/judicial-system/web/src/routes/PublicProsecutor/Indictments/ReviewDecision/ReviewDecision.css.ts rename to apps/judicial-system/web/src/routes/PublicProsecutor/components/ReviewDecision/ReviewDecision.css.ts diff --git a/apps/judicial-system/web/src/routes/PublicProsecutor/Indictments/ReviewDecision/ReviewDecision.strings.ts b/apps/judicial-system/web/src/routes/PublicProsecutor/components/ReviewDecision/ReviewDecision.strings.ts similarity index 100% rename from apps/judicial-system/web/src/routes/PublicProsecutor/Indictments/ReviewDecision/ReviewDecision.strings.ts rename to apps/judicial-system/web/src/routes/PublicProsecutor/components/ReviewDecision/ReviewDecision.strings.ts diff --git a/apps/judicial-system/web/src/routes/PublicProsecutor/Indictments/ReviewDecision/ReviewDecision.tsx b/apps/judicial-system/web/src/routes/PublicProsecutor/components/ReviewDecision/ReviewDecision.tsx similarity index 100% rename from apps/judicial-system/web/src/routes/PublicProsecutor/Indictments/ReviewDecision/ReviewDecision.tsx rename to apps/judicial-system/web/src/routes/PublicProsecutor/components/ReviewDecision/ReviewDecision.tsx diff --git a/apps/judicial-system/web/src/routes/Shared/IndictmentOverview/IndictmentOverview.tsx b/apps/judicial-system/web/src/routes/Shared/IndictmentOverview/IndictmentOverview.tsx index f47d9dfa8a36..c406f4e1ae4a 100644 --- a/apps/judicial-system/web/src/routes/Shared/IndictmentOverview/IndictmentOverview.tsx +++ b/apps/judicial-system/web/src/routes/Shared/IndictmentOverview/IndictmentOverview.tsx @@ -24,7 +24,7 @@ import { } from '@island.is/judicial-system-web/src/components' import { CaseState } from '@island.is/judicial-system-web/src/graphql/schema' -import { ReviewDecision } from '../../PublicProsecutor/Indictments/ReviewDecision/ReviewDecision' +import { ReviewDecision } from '../../PublicProsecutor/components/ReviewDecision/ReviewDecision' import { strings } from './IndictmentOverview.strings' const IndictmentOverview = () => { diff --git a/apps/native/app/package.json b/apps/native/app/package.json index 2526597d3f7d..aa43702e1444 100644 --- a/apps/native/app/package.json +++ b/apps/native/app/package.json @@ -59,6 +59,7 @@ "expo-notifications": "0.17.0", "intl": "1.2.5", "lodash": "4.17.21", + "path-to-regexp": "6.2.2", "react": "18.2.0", "react-intl": "5.20.12", "react-native": "0.71.1", @@ -116,6 +117,6 @@ "eslint": "8.12.0" }, "volta": { - "node": "18.16.0" + "extends": "../../../package.json" } } diff --git a/apps/native/app/src/config.ts b/apps/native/app/src/config.ts index 8706151e7b10..d79686855d0d 100644 --- a/apps/native/app/src/config.ts +++ b/apps/native/app/src/config.ts @@ -67,6 +67,8 @@ export const config = { '@island.is/finance/salary', '@island.is/finance/schedule:read', '@island.is/licenses:barcode', + '@island.is/notifications:read', + '@island.is/notifications:write', ], cognitoUrl: 'https://cognito.shared.devland.is/login', cognitoClientId: 'bre6r7d5e7imkcgbt7et1kqlc', diff --git a/apps/native/app/src/graphql/client.ts b/apps/native/app/src/graphql/client.ts index 19edd91e924e..e0ebe1cc16b9 100644 --- a/apps/native/app/src/graphql/client.ts +++ b/apps/native/app/src/graphql/client.ts @@ -17,10 +17,11 @@ import { openBrowser } from '../lib/rn-island' import { cognitoAuthUrl } from '../screens/cognito-auth/config-switcher' import { authStore } from '../stores/auth-store' import { environmentStore } from '../stores/environment-store' -import { apolloMKKVStorage } from '../stores/mkkv' +import { createMMKVStorage } from '../stores/mmkv' import { offlineStore } from '../stores/offline-store' import { MainBottomTabs } from '../utils/component-registry' -import { Alert } from 'react-native' + +const apolloMMKVStorage = createMMKVStorage({ withEncryption: true }) const connectivityLink = new ApolloLink((operation, forward) => { return forward(operation).map((response) => { @@ -195,7 +196,7 @@ export const getApolloClient = () => { export const initializeApolloClient = async () => { await persistCache({ cache, - storage: new MMKVStorageWrapper(apolloMKKVStorage), + storage: new MMKVStorageWrapper(apolloMMKVStorage), }) apolloClient = new ApolloClient({ diff --git a/apps/native/app/src/graphql/fragments/notification.fragment.graphql b/apps/native/app/src/graphql/fragments/notification.fragment.graphql new file mode 100644 index 000000000000..be128d893422 --- /dev/null +++ b/apps/native/app/src/graphql/fragments/notification.fragment.graphql @@ -0,0 +1,34 @@ +fragment NotificationMetadataFields on NotificationMetadata { + sent + updated + created + read +} + +fragment NotificationSenderFields on NotificationSender { + id + logoUrl +} + +fragment NotificationMessageFields on NotificationMessage { + title + body + displayBody + link { + url + } +} + +fragment NotificationDataFields on Notification { + id + notificationId + metadata { + ...NotificationMetadataFields + } + sender { + ...NotificationSenderFields + } + message { + ...NotificationMessageFields + } +} diff --git a/apps/native/app/src/graphql/queries/notifications.graphql b/apps/native/app/src/graphql/queries/notifications.graphql new file mode 100644 index 000000000000..788b25b6663c --- /dev/null +++ b/apps/native/app/src/graphql/queries/notifications.graphql @@ -0,0 +1,39 @@ +query GetUserNotifications($input: NotificationsInput!) { + userNotifications(input: $input) { + data { + ...NotificationDataFields + recipient { + nationalId + } + } + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + unreadCount + unseenCount + totalCount + } +} + +query GetUserNotificationsUnseenCount($input: NotificationsInput!) { + userNotifications(input: $input) { + unseenCount + } +} + +mutation MarkUserNotificationAsRead($id: Float!) { + markNotificationAsRead(id: $id) { + data { + ...NotificationDataFields + } + } +} + +mutation MarkAllNotificationsAsSeen { + markAllNotificationsSeen { + success + } +} diff --git a/apps/native/app/src/index.tsx b/apps/native/app/src/index.tsx index c5b93503d450..30e9613708ac 100644 --- a/apps/native/app/src/index.tsx +++ b/apps/native/app/src/index.tsx @@ -2,6 +2,7 @@ import { Navigation } from 'react-native-navigation' import { initializeApolloClient } from './graphql/client' import { readAuthorizeResult } from './stores/auth-store' import { showAppLockOverlay } from './utils/app-lock' +import { isAndroid } from './utils/devices' import { getDefaultOptions } from './utils/get-default-options' import { getAppRoot } from './utils/lifecycle/get-app-root' import { registerAllComponents } from './utils/lifecycle/setup-components' @@ -9,7 +10,7 @@ import { setupDevMenu } from './utils/lifecycle/setup-dev-menu' import { setupEventHandlers } from './utils/lifecycle/setup-event-handlers' import { setupGlobals } from './utils/lifecycle/setup-globals' import { - openInitialNotification, + handleInitialNotificationAndroid, setupNotifications, } from './utils/lifecycle/setup-notifications' import { setupRoutes } from './utils/lifecycle/setup-routes' @@ -52,7 +53,7 @@ async function startApp() { await Navigation.dismissAllOverlays() // Show lock screen overlay - showAppLockOverlay({ enforceActivated: true }) + void showAppLockOverlay({ enforceActivated: true }) // Dismiss all modals await Navigation.dismissAllModals() @@ -60,8 +61,10 @@ async function startApp() { // Set the app root await Navigation.setRoot({ root }) - // Open initial notification on android - openInitialNotification() + if (isAndroid) { + // Handle initial notification on android + handleInitialNotificationAndroid() + } // Mark app launched performanceMetricsAppLaunched() @@ -69,4 +72,4 @@ async function startApp() { } // Start the app -startApp() +void startApp() diff --git a/apps/native/app/src/lib/deep-linking.ts b/apps/native/app/src/lib/deep-linking.ts index 92fe6e1d1db6..96b546ae00db 100644 --- a/apps/native/app/src/lib/deep-linking.ts +++ b/apps/native/app/src/lib/deep-linking.ts @@ -1,8 +1,12 @@ +import { compile, match } from 'path-to-regexp' import { Navigation } from 'react-native-navigation' import createUse from 'zustand' import create, { State } from 'zustand/vanilla' import { bundleId } from '../config' -import { notificationsStore } from '../stores/notifications-store' +import { + GenericLicenseType, + NotificationMessage, +} from '../graphql/types/schema' import { ComponentRegistry, MainBottomTabs } from '../utils/component-registry' import { openBrowser } from './rn-island' @@ -152,6 +156,7 @@ export const resetSchemes = () => { const navigateTimeMap = new Map() const NAVIGATE_TIMEOUT = 500 + /** * Navigate to a specific url within the app * @param url Navigating url (ex. /inbox, /inbox/my-document-id, /wallet etc.) @@ -173,7 +178,7 @@ export function navigateTo(url: string, extraProps: any = {}) { // setup linking url const linkingUrl = `${bundleId}://${String(url).replace(/^\//, '')}` - // evalute and route + // evaluate and route return evaluateUrl(linkingUrl, extraProps) // @todo when to use native linking system? @@ -181,30 +186,79 @@ export function navigateTo(url: string, extraProps: any = {}) { } /** - * Navigate to a notification detail screen, or its link if defined. - * You may pass any one of its actions link if you want to go there as well. - * @param notification Notification object, requires `id` and an optional `link` - * @param componentId use specific componentId to open web browser in + * Navigate to a notification ClickActionUrl, if our mapping does not return a valid screen within the app - open a webview. */ -export function navigateToNotification( - notification: { id: string; link?: string }, - componentId?: string, -) { - const { id, link } = notification - // mark notification as read - if (id) { - notificationsStore.getState().actions.setRead(id) - const didNavigate = navigateTo(link ?? `/notification/${id}`) - if (!didNavigate && link) { - if (!componentId) { - // Use home tab for browser - Navigation.mergeOptions(MainBottomTabs, { - bottomTabs: { - currentTabIndex: 1, - }, - }) - } - openBrowser(link, componentId ?? ComponentRegistry.HomeScreen) +export function navigateToNotification({ + link, + componentId, +}: { + // url to navigate to + link?: NotificationMessage['link']['url'] + // componentId to open web browser in + componentId?: string +}) { + // If no link do nothing + if (!link) return + + const appRoute = findRoute(link) + + if (appRoute) { + navigateTo(appRoute) + + return + } + + if (!componentId) { + // Use home tab for browser + Navigation.mergeOptions(MainBottomTabs, { + bottomTabs: { + currentTabIndex: 1, + }, + }) + } + + void openBrowser(link, componentId ?? ComponentRegistry.HomeScreen) +} + +// Map between notification link and app screen +const urlMapping: { [key: string]: string } = { + '/minarsidur/postholf/:id': '/inbox/:id', + '/minarsidur/min-gogn/stillingar': '/settings', + '/minarsidur/skirteini': '/wallet', + '/minarsidur/skirteini/tjodskra/vegabref/:id': '/walletpassport/:id', + '/minarsidur/skirteini/:provider/ehic/:id': `/wallet/${GenericLicenseType.Ehic}`, + '/minarsidur/skirteini/:provider/veidikort/:id': `/wallet/${GenericLicenseType.HuntingLicense}`, + '/minarsidur/skirteini/:provider/pkort/:id': `/wallet/${GenericLicenseType.PCard}`, + '/minarsidur/skirteini/:provider/okurettindi/:id': `/wallet/${GenericLicenseType.DriversLicense}`, + '/minarsidur/skirteini/:provider/adrrettindi/:id': `/wallet/${GenericLicenseType.AdrLicense}`, + '/minarsidur/skirteini/:provider/vinnuvelarettindi/:id': `/wallet/${GenericLicenseType.MachineLicense}`, + '/minarsidur/skirteini/:provider/skotvopnaleyfi/:id': `/wallet/${GenericLicenseType.FirearmLicense}`, + '/minarsidur/skirteini/:provider/ororkuskirteini/:id': `/wallet/${GenericLicenseType.DisabilityLicense}`, + '/minarsidur/eignir/fasteignir': '/assets', + '/minarsidur/eignir/fasteignir/:id': '/asset/:id', + '/minarsidur/fjarmal/stada': '/finance', + '/minarsidur/eignir/okutaeki/min-okutaeki': '/vehicles', + '/minarsidur/eignir/okutaeki/min-okutaeki/:id': '/vehicle/:id', + '/minarsidur/eignir/okutaeki/min-okutaeki/:id/kilometrastada': + '/vehicle-mileage/:id', + '/minarsidur/loftbru': '/air-discount', +} + +const findRoute = (url: string) => { + // Remove trailing slash and spacess + const cleanLink = url.replace(/\/\s*$/, '') + // Remove domain + const path = cleanLink.replace(/https?:\/\/[^/]+/, '') + + for (const [pattern, routeTemplate] of Object.entries(urlMapping)) { + const matcher = match(pattern, { decode: decodeURIComponent }) + const matchResult = matcher(path) + + if (matchResult) { + const compiler = compile(routeTemplate) + return compiler(matchResult.params) } } + + return null } diff --git a/apps/native/app/src/messages/en.ts b/apps/native/app/src/messages/en.ts index 4858b037dc7e..4e9a342e8802 100644 --- a/apps/native/app/src/messages/en.ts +++ b/apps/native/app/src/messages/en.ts @@ -183,6 +183,7 @@ export const en: TranslatedMessages = { // document detail 'documentDetail.screenTitle': 'Document', 'documentDetail.loadingText': 'Loading document', + 'documentDetail.errorUnknown': 'Error occurred while loading document', // wallet 'wallet.screenTitle': 'Wallet', @@ -273,9 +274,7 @@ export const en: TranslatedMessages = { // notifications 'notifications.screenTitle': 'Notifications', - - // notification detail - 'notificationDetail.screenTitle': 'Notification', + 'notifications.errorUnknown': 'Error occurred while loading notifications', // profile 'profile.screenTitle': 'More', diff --git a/apps/native/app/src/messages/is.ts b/apps/native/app/src/messages/is.ts index a1a6f3fb72bd..21a23e9f5b43 100644 --- a/apps/native/app/src/messages/is.ts +++ b/apps/native/app/src/messages/is.ts @@ -182,6 +182,7 @@ export const is = { // document detail 'documentDetail.screenTitle': 'Skjal', 'documentDetail.loadingText': 'Sæki skjal', + 'documentDetail.errorUnknown': 'Villa kom upp við að sækja skjal', // profile 'profile.screenTitle': 'Meira', @@ -407,9 +408,7 @@ export const is = { // notifications 'notifications.screenTitle': 'Tilkynningar', - - // notification detail - 'notificationDetail.screenTitle': 'Tilkynning', + 'notifications.errorUnknown': 'Villa kom upp við að sækja tilkynningar', // applications screen 'applications.title': 'Umsóknir', diff --git a/apps/native/app/src/screens/home/home.tsx b/apps/native/app/src/screens/home/home.tsx index f45ebec60e2b..90d4551c728d 100644 --- a/apps/native/app/src/screens/home/home.tsx +++ b/apps/native/app/src/screens/home/home.tsx @@ -24,15 +24,13 @@ import { import { createNavigationOptionHooks } from '../../hooks/create-navigation-option-hooks' import { useActiveTabItemPress } from '../../hooks/use-active-tab-item-press' import { useConnectivityIndicator } from '../../hooks/use-connectivity-indicator' -import { notificationsStore } from '../../stores/notifications-store' +import { useNotificationsStore } from '../../stores/notifications-store' import { useUiStore } from '../../stores/ui-store' import { isAndroid } from '../../utils/devices' import { getRightButtons } from '../../utils/get-main-root' import { testIDs } from '../../utils/test-ids' import { ApplicationsModule } from './applications-module' -import { NotificationsModule } from './notifications-module' import { OnboardingModule } from './onboarding-module' -import { VehiclesModule } from './vehicles-module' interface ListItem { id: string @@ -101,6 +99,8 @@ export const MainHomeScreen: NavigationFunctionComponent = ({ }) => { useNavigationOptions(componentId) + const syncToken = useNotificationsStore(({ syncToken }) => syncToken) + const checkUnseen = useNotificationsStore(({ checkUnseen }) => checkUnseen) const [refetching, setRefetching] = useState(false) const flatListRef = useRef(null) const ui = useUiStore() @@ -132,8 +132,9 @@ export const MainHomeScreen: NavigationFunctionComponent = ({ const scrollY = useRef(new Animated.Value(0)).current useEffect(() => { - // Sync push tokens - notificationsStore.getState().actions.syncToken() + // Sync push tokens and unseen notifications + void syncToken() + void checkUnseen() }, []) const refetch = useCallback(async () => { @@ -170,16 +171,6 @@ export const MainHomeScreen: NavigationFunctionComponent = ({ /> ), }, - isMileageEnabled - ? { - id: 'vehicles', - component: , - } - : null, - { - id: 'notifications', - component: , - }, ].filter(Boolean) as Array<{ id: string component: React.JSX.Element diff --git a/apps/native/app/src/screens/home/notifications-module.tsx b/apps/native/app/src/screens/home/notifications-module.tsx deleted file mode 100644 index 611b6560920e..000000000000 --- a/apps/native/app/src/screens/home/notifications-module.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { Heading, NotificationCard } from '@ui' -import React, { useCallback } from 'react' -import { useIntl } from 'react-intl' -import { SafeAreaView } from 'react-native' -import { navigateToNotification } from '../../lib/deep-linking' -import { - Notification, - actionsForNotification, - useNotificationsStore, -} from '../../stores/notifications-store' -import { useOrganizationsStore } from '../../stores/organizations-store' - -interface NotificationsModuleProps { - componentId: string -} - -export const NotificationsModule = React.memo( - ({ componentId }: NotificationsModuleProps) => { - const intl = useIntl() - const { getNotifications } = useNotificationsStore() - const { getOrganizationLogoUrl } = useOrganizationsStore() - const notifications = getNotifications().slice(0, 5) - const onNotificationPress = useCallback((notification: Notification) => { - navigateToNotification(notification, componentId) - }, []) - - return ( - - {intl.formatMessage({ id: 'home.notifications' })} - {notifications.map((notification) => ( - onNotificationPress(notification)} - actions={actionsForNotification(notification, componentId)} - /> - ))} - - ) - }, -) diff --git a/apps/native/app/src/screens/home/vehicles-module.tsx b/apps/native/app/src/screens/home/vehicles-module.tsx deleted file mode 100644 index f0fcf01aa5ae..000000000000 --- a/apps/native/app/src/screens/home/vehicles-module.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { - ChevronRight, - Heading, - NotificationCard, - Typography, - blue400, -} from '@ui' -import React from 'react' -import { useIntl } from 'react-intl' -import { Image, SafeAreaView, TouchableOpacity, View } from 'react-native' -import vehicleIcon from '../../assets/icons/vehicle.png' -import { navigateTo } from '../../lib/deep-linking' - -export const VehiclesModule = React.memo(() => { - const intl = useIntl() - - return ( - - navigateTo('/vehicles')} - style={{ - flexDirection: 'row', - alignItems: 'center', - }} - > - - {intl.formatMessage({ id: 'button.seeAll' })} - - - - } - > - {intl.formatMessage({ id: 'profile.vehicles' })} - - navigateTo('/vehicles')} - icon={ - - - - } - title={intl.formatMessage({ id: 'vehicleDetail.odometer' })} - message={intl.formatMessage({ id: 'home.vehicleModule.summary' })} - actions={[ - { - text: intl.formatMessage({ id: 'home.vehicleModule.button' }), - onPress: () => navigateTo('/vehicles'), - }, - ]} - /> - - ) -}) diff --git a/apps/native/app/src/screens/notification-detail/notification-detail.tsx b/apps/native/app/src/screens/notification-detail/notification-detail.tsx deleted file mode 100644 index 9776e1b06160..000000000000 --- a/apps/native/app/src/screens/notification-detail/notification-detail.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { Content, dynamicColor, Header, NavigationBarSheet } from '@ui' -import React from 'react' -import { FormattedDate, useIntl } from 'react-intl' -import { - Navigation, - NavigationFunctionComponent, -} from 'react-native-navigation' -import styled from 'styled-components/native' -import { createNavigationOptionHooks } from '../../hooks/create-navigation-option-hooks' -import { - actionsForNotification, - useNotificationsStore, -} from '../../stores/notifications-store' -import { useOrganizationsStore } from '../../stores/organizations-store' -import { testIDs } from '../../utils/test-ids' - -interface NotificationDetailScreenProps { - id: string -} - -const Host = styled.SafeAreaView` - margin-left: 24px; - margin-right: 24px; - flex: 1; -` - -const Actions = styled.View` - flex-direction: row; - justify-content: center; - - padding-top: 8px; - - border-top-width: 1px; - border-top-color: ${dynamicColor( - (props) => ({ - dark: props.theme.shades.dark.shade200, - light: props.theme.color.blue100, - }), - true, - )}; -` - -const Action = styled.Button` - flex: 1; -` - -const { useNavigationOptions, getNavigationOptions } = - createNavigationOptionHooks(() => ({ - topBar: { - visible: false, - }, - })) - -export const NotificationDetailScreen: NavigationFunctionComponent< - NotificationDetailScreenProps -> = ({ componentId, id }) => { - useNavigationOptions(componentId) - const intl = useIntl() - const { items } = useNotificationsStore() - const { getOrganizationLogoUrl } = useOrganizationsStore() - const notification = items.get(id)! - const actions = actionsForNotification(notification, componentId) - - return ( - - Navigation.dismissModal(componentId)} - /> -
} - /> - - {actions.length > 0 && ( - - {actions.map((action, i) => ( - - ))} - - )} - - ) -} - -NotificationDetailScreen.options = getNavigationOptions diff --git a/apps/native/app/src/screens/notifications/notifications.tsx b/apps/native/app/src/screens/notifications/notifications.tsx index 108af8a36ce7..03adf84b246b 100644 --- a/apps/native/app/src/screens/notifications/notifications.tsx +++ b/apps/native/app/src/screens/notifications/notifications.tsx @@ -1,22 +1,39 @@ -import { NavigationBarSheet, NotificationCard } from '@ui' +import { NavigationBarSheet, NotificationCard, Skeleton } from '@ui' import { dismissAllNotificationsAsync } from 'expo-notifications' -import React, { useCallback, useEffect } from 'react' +import React, { useCallback, useEffect, useMemo, useState } from 'react' import { useIntl } from 'react-intl' -import { FlatList, SafeAreaView, TouchableHighlight } from 'react-native' +import { ActivityIndicator, FlatList, SafeAreaView } from 'react-native' import { Navigation, NavigationFunctionComponent, } from 'react-native-navigation' import { useTheme } from 'styled-components' +import styled from 'styled-components/native' +import { + GetUserNotificationsQuery, + Notification, + useGetUserNotificationsQuery, + useMarkAllNotificationsAsSeenMutation, + useMarkUserNotificationAsReadMutation, +} from '../../graphql/types/schema' + import { createNavigationOptionHooks } from '../../hooks/create-navigation-option-hooks' import { navigateToNotification } from '../../lib/deep-linking' +import { useNotificationsStore } from '../../stores/notifications-store' import { - Notification, - actionsForNotification, - useNotificationsStore, -} from '../../stores/notifications-store' -import { useOrganizationsStore } from '../../stores/organizations-store' + createSkeletonArr, + SkeletonItem, +} from '../../utils/create-skeleton-arr' +import { isAndroid } from '../../utils/devices' import { testIDs } from '../../utils/test-ids' +import { Problem } from '@ui/lib/problem/problem' + +const LoadingWrapper = styled.View` + padding-vertical: ${({ theme }) => theme.spacing[3]}px; + ${({ theme }) => isAndroid && `padding-bottom: ${theme.spacing[6]}px;`} +` + +const DEFAULT_PAGE_SIZE = 50 const { getNavigationOptions, useNavigationOptions } = createNavigationOptionHooks(() => ({ @@ -25,49 +42,142 @@ const { getNavigationOptions, useNavigationOptions } = }, })) +type NotificationItem = NonNullable< + NonNullable['data'] +>[0] + +type ListItem = SkeletonItem | NotificationItem + export const NotificationsScreen: NavigationFunctionComponent = ({ componentId, }) => { useNavigationOptions(componentId) - const { getNotifications } = useNotificationsStore() const intl = useIntl() const theme = useTheme() - const notifications = getNotifications() - const { getOrganizationLogoUrl } = useOrganizationsStore() + const [loadingMore, setLoadingMore] = useState(false) + const updateNavigationUnseenCount = useNotificationsStore( + ({ updateNavigationUnseenCount }) => updateNavigationUnseenCount, + ) - const onNotificationPress = useCallback((notification: Notification) => { - navigateToNotification(notification, componentId) + const { data, loading, error, fetchMore } = useGetUserNotificationsQuery({ + variables: { input: { limit: DEFAULT_PAGE_SIZE } }, + }) + + const [markUserNotificationAsRead] = useMarkUserNotificationAsReadMutation() + const [markAllUserNotificationsAsSeen] = + useMarkAllNotificationsAsSeenMutation() + + // On mount, mark all notifications as seen and update all screens navigation badge to 0 + useEffect(() => { + void markAllUserNotificationsAsSeen().then(() => + updateNavigationUnseenCount(0), + ) }, []) useEffect(() => { - dismissAllNotificationsAsync() + void dismissAllNotificationsAsync() }) - const renderNotificationItem = ({ item }: { item: Notification }) => { + const memoizedData = useMemo(() => { + if (loading && !data) { + return createSkeletonArr(9) + } + + return data?.userNotifications?.data || [] + }, [data, loading]) + + const onNotificationPress = useCallback((notification: Notification) => { + // Mark notification as read and seen + void markUserNotificationAsRead({ variables: { id: notification.id } }) + + navigateToNotification({ + componentId, + link: notification.message?.link?.url, + }) + }, []) + + const handleEndReached = async () => { + if (loadingMore || loading) return + + setLoadingMore(true) + + try { + await fetchMore({ + variables: { + input: { + limit: DEFAULT_PAGE_SIZE, + after: data?.userNotifications?.pageInfo.endCursor ?? undefined, + }, + }, + updateQuery: (prev, { fetchMoreResult }) => { + if ( + !fetchMoreResult || + !fetchMoreResult.userNotifications?.pageInfo?.hasNextPage + ) { + return prev + } + + return { + userNotifications: { + ...fetchMoreResult.userNotifications, + data: [ + ...(prev.userNotifications?.data || []), + ...(fetchMoreResult.userNotifications?.data || []), + ], + }, + } + }, + }) + } catch (e) { + // TODO handle error + } + + setLoadingMore(false) + } + + const renderNotificationItem = ({ item }: { item: ListItem }) => { + if (item.__typename === 'Skeleton') { + return ( + + ) + } + return ( - onNotificationPress(item)} testID={testIDs.NOTIFICATION_CARD_BUTTON} - > - onNotificationPress(item)} - actions={actionsForNotification(item, componentId)} - /> - + /> ) } + const keyExtractor = useCallback( + (item: ListItem) => item.id.toString(), + [memoizedData], + ) + return ( <> Navigation.dismissModal(componentId)} style={{ marginHorizontal: 16 }} /> - - item.id} - renderItem={renderNotificationItem} - /> + + {error ? ( + + ) : ( + + + + ) : null + } + /> + )} ) diff --git a/apps/native/app/src/screens/settings/settings.tsx b/apps/native/app/src/screens/settings/settings.tsx index b4b7f65e3cd9..77665250eab7 100644 --- a/apps/native/app/src/screens/settings/settings.tsx +++ b/apps/native/app/src/screens/settings/settings.tsx @@ -1,5 +1,4 @@ import { useApolloClient } from '@apollo/client' -import messaging from '@react-native-firebase/messaging' import { Alert, NavigationBarSheet, @@ -40,7 +39,8 @@ import { createNavigationOptionHooks } from '../../hooks/create-navigation-optio import { navigateTo } from '../../lib/deep-linking' import { showPicker } from '../../lib/show-picker' import { authStore } from '../../stores/auth-store' -import { apolloMKKVStorage } from '../../stores/mkkv' +import { clearAllStorages } from '../../stores/mmkv' +import { useNotificationsStore } from '../../stores/notifications-store' import { preferencesStore, usePreferencesStore, @@ -78,16 +78,28 @@ export const SettingsScreen: NavigationFunctionComponent = ({ setUseBiometrics, appLockTimeout, } = usePreferencesStore() + const pushToken = useNotificationsStore(({ pushToken }) => pushToken) + const deletePushToken = useNotificationsStore( + ({ deletePushToken }) => deletePushToken, + ) + const resetNotificationsStore = useNotificationsStore(({ reset }) => reset) const [loadingCP, setLoadingCP] = useState(false) const [localPackage, setLocalPackage] = useState(null) - const [pushToken, setPushToken] = useState('loading...') const efficient = useRef({}).current const isInfoDismissed = dismissed.includes('userSettingsInformational') const { authenticationTypes, isEnrolledBiometrics } = useUiStore() const biometricType = useBiometricType(authenticationTypes) const onLogoutPress = async () => { - apolloMKKVStorage.clearStore() + if (pushToken) { + await deletePushToken(pushToken) + } + + resetNotificationsStore() + + // Clear all MMKV storages + void clearAllStorages() + await authStore.getState().logout() await Navigation.dismissAllModals() await Navigation.setRoot({ @@ -128,10 +140,6 @@ export const SettingsScreen: NavigationFunctionComponent = ({ setLoadingCP(false) setLocalPackage(p) }) - messaging() - .getToken() - .then((token) => setPushToken(token)) - .catch(() => setPushToken('no token in simulator')) }, 330) }, []) diff --git a/apps/native/app/src/screens/wallet-pass/wallet-pass.tsx b/apps/native/app/src/screens/wallet-pass/wallet-pass.tsx index 714b7f909b49..b8ccf076b3c7 100644 --- a/apps/native/app/src/screens/wallet-pass/wallet-pass.tsx +++ b/apps/native/app/src/screens/wallet-pass/wallet-pass.tsx @@ -148,7 +148,7 @@ export const WalletPassScreen: NavigationFunctionComponent<{ fetchPolicy: 'network-only', variables: { input: { - licenseType: item?.license.type ?? '', + licenseType: item?.license.type ?? id, }, }, }) @@ -341,6 +341,17 @@ export const WalletPassScreen: NavigationFunctionComponent<{ ) } + // If we don't have an item we want to return a loading spinner for the whole screen to prevent showing the wrong license while fetching + if (loading && !item) { + return ( + + ) + } + return ( diff --git a/apps/native/app/src/stores/auth-store.ts b/apps/native/app/src/stores/auth-store.ts index 2bacdd08ecf6..a126f77caaf9 100644 --- a/apps/native/app/src/stores/auth-store.ts +++ b/apps/native/app/src/stores/auth-store.ts @@ -19,6 +19,9 @@ import { preferencesStore } from './preferences-store' const KEYCHAIN_AUTH_KEY = `@islandis_${bundleId}` +// Optional scopes (not required for all users so we do not want to force a logout) +const OPTIONAL_SCOPES = ['@island.is/licenses:barcode'] + interface UserInfo { sub: string nationalId: string @@ -199,7 +202,10 @@ export async function checkIsAuthenticated() { } if ('scopes' in authorizeResult) { - const hasRequiredScopes = appAuthConfig.scopes.every((scope) => + const requiredScopes = appAuthConfig.scopes.filter( + (scope) => !OPTIONAL_SCOPES.includes(scope), + ) + const hasRequiredScopes = requiredScopes.every((scope) => authorizeResult.scopes.includes(scope), ) if (!hasRequiredScopes) { diff --git a/apps/native/app/src/stores/mkkv.ts b/apps/native/app/src/stores/mkkv.ts deleted file mode 100644 index 15b4295cf995..000000000000 --- a/apps/native/app/src/stores/mkkv.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { MMKVLoader } from 'react-native-mmkv-storage' - -export const apolloMKKVStorage = new MMKVLoader() - .withEncryption() // Generates a random key and stores it securely in Keychain - .initialize() diff --git a/apps/native/app/src/stores/mmkv.ts b/apps/native/app/src/stores/mmkv.ts new file mode 100644 index 000000000000..ff750df3c40c --- /dev/null +++ b/apps/native/app/src/stores/mmkv.ts @@ -0,0 +1,29 @@ +import { MMKVInstance, MMKVLoader } from 'react-native-mmkv-storage' +import { StateStorage } from 'zustand/middleware' + +const storages: MMKVInstance[] = [] + +export const createMMKVStorage = ({ + withEncryption = false, +}: { withEncryption?: boolean } = {}): MMKVInstance => { + const storage = withEncryption + ? new MMKVLoader() + .withEncryption() // Generates a random key and stores it securely in Keychain + .initialize() + : new MMKVLoader().initialize() + + storages.push(storage) + + return storage +} + +export const clearAllStorages = () => { + storages.forEach((storage) => storage.clearStore()) +} + +export const createZustandMMKVStorage = ( + storage: MMKVInstance, +): StateStorage => ({ + getItem: (name) => storage.getString(name) ?? null, + setItem: (name, value) => storage.setItem(name, value) as Promise, +}) diff --git a/apps/native/app/src/stores/notifications-store.ts b/apps/native/app/src/stores/notifications-store.ts index 91a30408bd98..5dde4198b479 100644 --- a/apps/native/app/src/stores/notifications-store.ts +++ b/apps/native/app/src/stores/notifications-store.ts @@ -1,6 +1,5 @@ import AsyncStorage from '@react-native-community/async-storage' import messaging from '@react-native-firebase/messaging' -import { NotificationResponse } from 'expo-notifications' import { Navigation } from 'react-native-navigation' import createUse from 'zustand' import { persist } from 'zustand/middleware' @@ -8,11 +7,15 @@ import create, { State } from 'zustand/vanilla' import { getApolloClientAsync } from '../graphql/client' import { AddUserProfileDeviceTokenDocument, + AddUserProfileDeviceTokenMutation, AddUserProfileDeviceTokenMutationVariables, DeleteUserProfileDeviceTokenDocument, + DeleteUserProfileDeviceTokenMutation, DeleteUserProfileDeviceTokenMutationVariables, + GetUserNotificationsUnseenCountDocument, + GetUserNotificationsUnseenCountQuery, + GetUserNotificationsUnseenCountQueryVariables, } from '../graphql/types/schema' -import { navigateToNotification } from '../lib/deep-linking' import { ComponentRegistry } from '../utils/component-registry' import { getRightButtons } from '../utils/get-main-root' @@ -28,257 +31,139 @@ export interface Notification { read: boolean } -interface NotificationsStore extends State { - items: Map - unreadCount: number +interface NotificationsState extends State { + unseenCount: number pushToken?: string - getNotifications(): Notification[] - actions: { - syncToken(): Promise - handleNotificationResponse(response: NotificationResponse): Notification - setRead(notificationId: string): void - setUnread(notificationId: string): void - } } -const firstNotification: Notification = { - id: 'FIRST_NOTIFICATION', - title: 'Stafrænt Ísland', - body: 'Fyrsta útgáfa af Ísland.is appinu', - copy: 'Í þessari fyrstu útgáfu af Ísland.is appinu getur þú nálgast rafræn skjöl og skírteini frá hinu opinbera, fengið tilkynningar og séð stöðu umsókna.', - date: new Date().getTime(), - data: {}, - read: true, +interface NotificationsActions { + syncToken(): Promise + checkUnseen(): Promise + updateNavigationUnseenCount(unseenCount: number): void + deletePushToken(pushToken: string): Promise + reset(): void } -export const notificationCategories = [ - { - categoryIdentifier: 'NEW_DOCUMENT', - actions: [ - { - identifier: 'ACTION_OPEN_DOCUMENT', - buttonTitle: 'Opna', - onPress: ({ id, data }: Notification, componentId?: string) => { - return navigateToNotification({ id, link: data.url }, componentId) - }, - }, - { - identifier: 'ACTION_MARK_AS_READ', - buttonTitle: 'Merkja sem lesið', - onPress: ({ id }: Notification) => - notificationsStore.getState().actions.setRead(id), - }, - ], - data: { - documentId: '', - }, - }, - { - categoryIdentifier: 'ISLANDIS_LINK', - actions: [ - { - identifier: 'ACTION_OPEN_ON_ISLAND_IS', - buttonTitle: 'Opna á Ísland.is', - onPress: ({ id, data }: Notification, componentId?: string) => - navigateToNotification({ id, link: data.islandIsUrl }, componentId), - }, - { - identifier: 'ACTION_MARK_AS_READ', - buttonTitle: 'Merkja sem lesið', - onPress: ({ id }: Notification) => - notificationsStore.getState().actions.setRead(id), - }, - ], - data: { - islandIsUrl: '', - }, - }, -] +type NotificationsStore = NotificationsState & NotificationsActions const rightButtonScreens = [ ComponentRegistry.HomeScreen, ComponentRegistry.InboxScreen, ComponentRegistry.WalletScreen, ComponentRegistry.ApplicationsScreen, + ComponentRegistry.MoreScreen, ] -export function actionsForNotification( - notification: Notification, - componentId?: string, -) { - const category = notificationCategories.find( - (c) => c.categoryIdentifier === notification.category, - ) - if (category) { - return category.actions - .filter((action) => action.identifier !== 'ACTION_MARK_AS_READ') - .map((action) => ({ - text: action.buttonTitle, - onPress: () => action.onPress(notification, componentId), - })) - } - if (notification.data.url) { - return [ - { - text: 'Opna viðhengi', - onPress: () => - navigateToNotification( - { id: notification.id, link: notification.data.url }, - componentId, - ), - }, - ] - } - - return [] +const initialState: NotificationsState = { + unseenCount: 0, + pushToken: undefined, } export const notificationsStore = create( persist( (set, get) => ({ - items: new Map(), - unreadCount: 0, - pushToken: undefined, - getNotifications() { - return [...get().items.values()].sort((a, b) => b.date - a.date) - }, - actions: { - async syncToken() { - const client = await getApolloClientAsync() - const token = await messaging().getToken() - const { pushToken } = get() - - if (pushToken !== token) { - if (pushToken) { - // Attempt to remove old push token - try { - await client.mutate< - object, - DeleteUserProfileDeviceTokenMutationVariables - >({ - mutation: DeleteUserProfileDeviceTokenDocument, - variables: { - input: { - deviceToken: pushToken, - }, - }, - }) - } catch (err) { - // noop - console.error('Error removing old push token', err) - } - } + ...initialState, - try { - // Register the new push token - const res = await client.mutate< - object, - AddUserProfileDeviceTokenMutationVariables - >({ - mutation: AddUserProfileDeviceTokenDocument, - variables: { - input: { - deviceToken: token, - }, - }, - }) - - console.log('Registered push token', res) - // Update push token in store - set({ pushToken: token }) - } catch (err) { - console.log('Failed to register push token', err) - } - } - }, - handleNotificationResponse(response: NotificationResponse) { - const { items } = get() - const { - date, - request: { content, identifier, trigger }, - } = response.notification + async syncToken() { + const client = await getApolloClientAsync() + const token = await messaging().getToken() + const { pushToken: oldToken, deletePushToken } = get() - if (items.has(identifier)) { - // ignore notification model updates - return items.get(identifier)! + if (oldToken !== token) { + if (oldToken) { + await deletePushToken(oldToken) } - const data = { - ...(content.data || {}), - ...((trigger as any).payload || {}), - } - const model = { - id: identifier, - date: date * 1000, - category: (content as any).categoryIdentifier, - title: content.title ?? '', - subtitle: content.subtitle || undefined, - body: content.body || undefined, - copy: data.copy, - data, - read: false, - } - items.set(model.id, model) - set({ items: new Map(items) }) - return model - }, - setRead(notificationId: string) { - const { items } = get() - const notification = items.get(notificationId) - if (notification) { - notification.read = true - } - set({ items: new Map(items) }) - }, - setUnread(notificationId: string) { - const { items } = get() - const notification = items.get(notificationId) - if (notification) { - notification.read = false + try { + // Register the new push token + const res = await client.mutate< + AddUserProfileDeviceTokenMutation, + AddUserProfileDeviceTokenMutationVariables + >({ + mutation: AddUserProfileDeviceTokenDocument, + variables: { + input: { + deviceToken: token, + }, + }, + }) + + console.log('Registered push token', res) + // Update push token in store + set({ pushToken: token }) + } catch (err) { + console.log('Failed to register push token', err) } - set({ items: new Map(items) }) - }, + } + }, + async deletePushToken(deviceToken: string) { + const client = await getApolloClientAsync() + + // Attempt to remove old push token + try { + await client.mutate< + DeleteUserProfileDeviceTokenMutation, + DeleteUserProfileDeviceTokenMutationVariables + >({ + mutation: DeleteUserProfileDeviceTokenDocument, + variables: { + input: { + deviceToken, + }, + }, + }) + + set({ + pushToken: undefined, + }) + } catch (err) { + // noop + console.error('Error removing old push token', err) + } + }, + updateNavigationUnseenCount(unseenCount: number) { + set({ unseenCount }) + + rightButtonScreens.forEach((componentId) => { + Navigation.mergeOptions(componentId, { + topBar: { + rightButtons: getRightButtons({ unseenCount }), + }, + }) + }) + }, + async checkUnseen() { + const client = await getApolloClientAsync() + + try { + const res = await client.query< + GetUserNotificationsUnseenCountQuery, + GetUserNotificationsUnseenCountQueryVariables + >({ + query: GetUserNotificationsUnseenCountDocument, + fetchPolicy: 'network-only', + variables: { + input: { + limit: 1, + }, + }, + }) + + const unseenCount = res?.data?.userNotifications?.unseenCount ?? 0 + get().updateNavigationUnseenCount(unseenCount) + } catch (err) { + // noop + } + }, + reset() { + set(initialState) }, }), { - name: 'notifications_06', + name: 'notifications_07', getStorage: () => AsyncStorage, - serialize({ state, version }) { - const res: any = { ...state } - res.items = [...res.items] - return JSON.stringify({ state: res, version }) - }, - deserialize(str: string) { - const { state, version } = JSON.parse(str) - delete state.actions - state.items = new Map(state.items) - return { state, version } - }, }, ), ) -notificationsStore.subscribe( - (items: Map) => { - const unreadCount = [...items.values()].reduce((acc, item) => { - return acc + (item.read ? 0 : 1) - }, 0) - notificationsStore.setState({ unreadCount }) - rightButtonScreens.forEach((componentId) => { - Navigation.mergeOptions(componentId, { - topBar: { - rightButtons: getRightButtons({ unreadCount }), - }, - }) - }) - }, - (s) => s.items, -) - -if (notificationsStore.getState().items.size === 0) { - const { items } = notificationsStore.getState() - items.set(firstNotification.id, firstNotification) - notificationsStore.setState({ items }) -} - export const useNotificationsStore = createUse(notificationsStore) diff --git a/apps/native/app/src/ui/lib/card/notification-card.tsx b/apps/native/app/src/ui/lib/card/notification-card.tsx index 576b475d2a12..47a3066988ec 100644 --- a/apps/native/app/src/ui/lib/card/notification-card.tsx +++ b/apps/native/app/src/ui/lib/card/notification-card.tsx @@ -1,186 +1,124 @@ import React, { isValidElement } from 'react' import { FormattedDate } from 'react-intl' -import { Image, ImageSourcePropType } from 'react-native' +import { ColorValue, Image, ImageSourcePropType } from 'react-native' import styled, { useTheme } from 'styled-components/native' import { dynamicColor } from '../../utils/dynamic-color' -import { font } from '../../utils/font' +import { Typography } from '../typography/typography' -const Host = styled.TouchableHighlight` - border-radius: ${({ theme }) => theme.border.radius.large}; +const Host = styled.TouchableHighlight<{ unread?: boolean }>` background-color: ${dynamicColor((props) => ({ - dark: 'shade100', - light: props.theme.color.blueberry100, + dark: props.unread ? props.theme.shades.dark.shade300 : 'transparent', + light: props.unread ? props.theme.color.blue100 : 'transparent', }))}; - margin-bottom: ${({ theme }) => theme.spacing[2]}px; ` -const Container = styled.View` - padding-top: 20px; -` - -const Title = styled.View` - flex-direction: row; - align-items: center; - flex: 1; - padding-right: ${({ theme }) => theme.spacing[1]}px; -` - -const ActionsContainer = styled.View` - border-top-width: ${({ theme }) => theme.border.width.standard}px; - border-top-color: ${dynamicColor( - (props) => ({ - dark: 'shade200', - light: props.theme.color.blueberry200, - }), - true, - )}; - flex-direction: row; -` - -const ActionButton = styled.TouchableOpacity<{ border: boolean }>` - flex: 1; - align-items: center; - justify-content: center; - padding: ${({ theme }) => theme.spacing[2]}px; - border-left-width: ${({ theme }) => theme.border.width.standard}px; - border-left-color: ${dynamicColor( - (props) => ({ - dark: !props.border ? 'transparent' : 'shade200', - light: !props.border ? 'transparent' : props.theme.color.blueberry200, +const Cell = styled.View` + padding-horizontal: ${({ theme }) => theme.spacing[2]}px; + padding-vertical: ${({ theme }) => theme.spacing[3]}px; + border-bottom-width: ${({ theme }) => theme.border.width.standard}px; + border-bottom-color: ${dynamicColor( + ({ theme }) => ({ + light: theme.color.blue200, + dark: theme.shades.dark.shade400, }), true, )}; ` -const ActionText = styled.Text` - ${font({ - fontWeight: '600', - color: ({ theme }) => theme.color.blue400, - })} - text-align: center; -` - -const TitleText = styled.Text` - flex: 1; - - ${font({ - fontWeight: '600', - fontSize: 13, - lineHeight: 17, - color: 'foreground', - })} -` - -const Row = styled.View` +const Container = styled.View` flex-direction: row; - justify-content: space-between; - margin-bottom: ${({ theme }) => theme.spacing[1]}px; - padding-left: ${({ theme }) => theme.spacing[2]}px; - padding-right: ${({ theme }) => theme.spacing[2]}px; + align-items: flex-start; + justify-content: flex-start; + column-gap: ${({ theme }) => theme.spacing[2]}px; ` -const Date = styled.View` +const Heading = styled.View` flex-direction: row; - align-items: center; + justify-content: space-around; + align-items: flex-start; ` -const DateText = styled.Text<{ unread?: boolean }>` - ${font({ - fontWeight: (props) => (props.unread ? '600' : '300'), - fontSize: 13, - lineHeight: 17, - color: 'foreground', - })} -` - -const Content = styled.View` - padding-left: ${({ theme }) => theme.spacing[2]}px; - padding-right: ${({ theme }) => theme.spacing[2]}px; - padding-bottom: 20px; -` - -const Message = styled.Text` - ${font({ - fontWeight: '300', - color: 'foreground', - lineHeight: 24, - })} +const Icon = styled.View<{ unread?: boolean }>` + background-color: ${({ theme, unread }) => + unread ? theme.color.white : theme.color.blue100}; + height: 40px; + width: 40px; + align-items: center; + justify-content: center; + border-radius: ${({ theme }) => theme.border.radius.circle}; + flex-direction: column; ` -const Dot = styled.View` - width: ${({ theme }) => theme.spacing[1]}px; - height: ${({ theme }) => theme.spacing[1]}px; - border-radius: ${({ theme }) => theme.border.radius.large}; - background-color: ${dynamicColor(({ theme }) => theme.color.blueberry400)}; - margin-left: ${({ theme }) => theme.spacing[1]}px; +const Row = styled.View` + flex-direction: column; + flex: 1; + row-gap: ${({ theme }) => theme.spacing.smallGutter}px; ` interface CardProps { - id: string + id: number | string icon?: ImageSourcePropType | React.ReactNode - category?: string date?: Date title: string message: string unread?: boolean - actions?: Array<{ text: string; onPress(): void }> - onPress(id: string): void + actions?: Array<{ text: string; onPress(id: string | number): void }> + underlayColor?: ColorValue + testID?: string + onPress(id: number | string): void } export function NotificationCard({ id, onPress, - category, title, message, date, icon, unread, - actions = [], + underlayColor, + testID, }: CardProps) { const theme = useTheme() + return ( onPress(id)} - underlayColor={theme.isDark ? theme.shade.shade200 : '#EBEBFA'} + underlayColor={ + underlayColor ?? theme.isDark + ? theme.shade.shade400 + : theme.color.blue100 + } + unread={unread} + testID={testID} > - - - - {icon && isValidElement(icon) ? ( - icon - ) : ( + <Cell> + <Container> + {icon && isValidElement(icon) ? ( + icon + ) : icon ? ( + <Icon unread={unread}> <Image source={icon as ImageSourcePropType} - style={{ width: 16, height: 16, marginRight: 8 }} + style={{ width: 16, height: 16 }} /> - )} - <TitleText numberOfLines={1} ellipsizeMode="tail"> - {title} - </TitleText> - - {date && ( - - - - - {unread && } - - )} - - - {message} - - {actions.length ? ( - - {actions.map(({ text, onPress }, i) => ( - - {text} - - ))} - - ) : null} - + + ) : null} + + + + {title} + + {date && ( + + + + )} + + {message} + + + ) } diff --git a/apps/native/app/src/ui/utils/theme.ts b/apps/native/app/src/ui/utils/theme.ts index 771a13623200..ce97652ea88a 100644 --- a/apps/native/app/src/ui/utils/theme.ts +++ b/apps/native/app/src/ui/utils/theme.ts @@ -82,7 +82,7 @@ export const theme = { standard: '4px', large: '8px', extraLarge: '16px', - circle: '50%', + circle: '100px', }, width: { standard: 1, diff --git a/apps/native/app/src/utils/component-registry.ts b/apps/native/app/src/utils/component-registry.ts index 42065031e026..60cc672f73bb 100644 --- a/apps/native/app/src/utils/component-registry.ts +++ b/apps/native/app/src/utils/component-registry.ts @@ -22,7 +22,6 @@ export const ComponentRegistry = { WalletPassportScreen: `${prefix}.screens.WalletPassport`, DocumentDetailScreen: `${prefix}.screens.DocumentDetail`, NotificationsScreen: `${prefix}.screens.Notifications`, - NotificationDetailScreen: `${prefix}.screens.NotificationDetail`, WebViewScreen: `${prefix}.screens.WebViewScreen`, LicenseScannerScreen: `${prefix}.screens.LicenseScannerScreen`, LicenseScanDetailScreen: `${prefix}.screens.LicenseScanDetailScreen`, diff --git a/apps/native/app/src/utils/create-skeleton-arr.ts b/apps/native/app/src/utils/create-skeleton-arr.ts new file mode 100644 index 000000000000..5d44ac9b262f --- /dev/null +++ b/apps/native/app/src/utils/create-skeleton-arr.ts @@ -0,0 +1,10 @@ +export type SkeletonItem = { + id: number + __typename: 'Skeleton' +} + +export const createSkeletonArr = (size: number): SkeletonItem[] => + Array.from({ length: size }, (_, id) => ({ + id, + __typename: 'Skeleton', + })) diff --git a/apps/native/app/src/utils/get-main-root.ts b/apps/native/app/src/utils/get-main-root.ts index 198f0122c260..1b4506a69a54 100644 --- a/apps/native/app/src/utils/get-main-root.ts +++ b/apps/native/app/src/utils/get-main-root.ts @@ -11,7 +11,7 @@ import { getThemeWithPreferences } from './get-theme-with-preferences' import { testIDs } from './test-ids' export const getRightButtons = ({ - unreadCount = notificationsStore.getState().unreadCount, + unseenCount = notificationsStore.getState().unseenCount, theme = getThemeWithPreferences(preferencesStore.getState()), } = {}): OptionsTopBarButton[] => { const iconBackground = { @@ -34,7 +34,7 @@ export const getRightButtons = ({ id: ButtonRegistry.NotificationsButton, testID: testIDs.TOPBAR_NOTIFICATIONS_BUTTON, icon: - unreadCount > 0 + unseenCount > 0 ? require('../assets/icons/topbar-notifications-bell.png') : require('../assets/icons/topbar-notifications.png'), iconBackground, diff --git a/apps/native/app/src/utils/lifecycle/setup-components.tsx b/apps/native/app/src/utils/lifecycle/setup-components.tsx index 29aa34fb1010..22fb3d7f87f2 100644 --- a/apps/native/app/src/utils/lifecycle/setup-components.tsx +++ b/apps/native/app/src/utils/lifecycle/setup-components.tsx @@ -21,7 +21,6 @@ import { LoginScreen } from '../../screens/login/login' import { TestingLoginScreen } from '../../screens/login/testing-login' import { MoreScreen } from '../../screens/more/more' import { PersonalInfoScreen } from '../../screens/more/personal-info' -import { NotificationDetailScreen } from '../../screens/notification-detail/notification-detail' import { NotificationsScreen } from '../../screens/notifications/notifications' import { OnboardingBiometricsScreen } from '../../screens/onboarding/onboarding-biometrics' import { OnboardingNotificationsScreen } from '../../screens/onboarding/onboarding-notifications' @@ -77,7 +76,6 @@ export function registerAllComponents() { registerComponent(CR.WalletPassportScreen, WalletPassportScreen) registerComponent(CR.DocumentDetailScreen, DocumentDetailScreen) registerComponent(CR.NotificationsScreen, NotificationsScreen) - registerComponent(CR.NotificationDetailScreen, NotificationDetailScreen) registerComponent(CR.WebViewScreen, WebViewScreen) registerComponent(CR.LicenseScannerScreen, LicenseScannerScreen) registerComponent(CR.LicenseScanDetailScreen, LicenseScanDetailScreen) diff --git a/apps/native/app/src/utils/lifecycle/setup-dev-menu.ts b/apps/native/app/src/utils/lifecycle/setup-dev-menu.ts index 89bf0181ccf6..0021ae4b6d28 100644 --- a/apps/native/app/src/utils/lifecycle/setup-dev-menu.ts +++ b/apps/native/app/src/utils/lifecycle/setup-dev-menu.ts @@ -4,7 +4,7 @@ import { ActionSheetIOS, DevSettings } from 'react-native' import DialogAndroid from 'react-native-dialogs' import { Navigation } from 'react-native-navigation' import { authStore } from '../../stores/auth-store' -import { apolloMKKVStorage } from '../../stores/mkkv' +import { clearAllStorages } from '../../stores/mmkv' import { preferencesStore } from '../../stores/preferences-store' import { ComponentRegistry } from '../component-registry' import { isAndroid, isIos } from '../devices' @@ -110,7 +110,7 @@ export function setupDevMenu() { case 'CLEAR_ASYNC_STORAGE': return clearAsyncStorage() case 'CLEAR_MKKV': - return apolloMKKVStorage.clearStore() + return clearAllStorages() } } diff --git a/apps/native/app/src/utils/lifecycle/setup-event-handlers.ts b/apps/native/app/src/utils/lifecycle/setup-event-handlers.ts index 9dcc9dffd58b..eaf73bdd70d2 100644 --- a/apps/native/app/src/utils/lifecycle/setup-event-handlers.ts +++ b/apps/native/app/src/utils/lifecycle/setup-event-handlers.ts @@ -1,6 +1,5 @@ import { addEventListener } from '@react-native-community/netinfo' import { impactAsync, ImpactFeedbackStyle } from 'expo-haptics' -import { getPresentedNotificationsAsync } from 'expo-notifications' import { AppState, AppStateStatus, @@ -12,6 +11,7 @@ import SpotlightSearch from 'react-native-spotlight-search' import { evaluateUrl, navigateTo } from '../../lib/deep-linking' import { authStore } from '../../stores/auth-store' import { environmentStore } from '../../stores/environment-store' +import { notificationsStore } from '../../stores/notifications-store' import { offlineStore } from '../../stores/offline-store' import { preferencesStore } from '../../stores/preferences-store' import { uiStore } from '../../stores/ui-store' @@ -24,8 +24,6 @@ import { ButtonRegistry, ComponentRegistry as CR } from '../component-registry' import { isIos } from '../devices' import { handleQuickAction } from '../quick-actions' -import { handleNotificationResponse } from './setup-notifications' - let backgroundAppLockTimeout: ReturnType export function setupEventHandlers() { @@ -94,14 +92,7 @@ export function setupEventHandlers() { const { appLockTimeout } = preferencesStore.getState() if (status === 'active') { - getPresentedNotificationsAsync().then((notifications) => { - notifications.forEach((notification) => - handleNotificationResponse({ - notification, - actionIdentifier: 'NOOP', - }), - ) - }) + void notificationsStore.getState().checkUnseen() } if (!skipAppLock()) { diff --git a/apps/native/app/src/utils/lifecycle/setup-notifications.ts b/apps/native/app/src/utils/lifecycle/setup-notifications.ts index f936b6ce74e9..6e9264a209b2 100644 --- a/apps/native/app/src/utils/lifecycle/setup-notifications.ts +++ b/apps/native/app/src/utils/lifecycle/setup-notifications.ts @@ -2,73 +2,31 @@ import messaging, { FirebaseMessagingTypes, } from '@react-native-firebase/messaging' import { + addNotificationReceivedListener, + addNotificationResponseReceivedListener, DEFAULT_ACTION_IDENTIFIER, Notification, NotificationResponse, - addNotificationReceivedListener, - addNotificationResponseReceivedListener, - setNotificationCategoryAsync, setNotificationHandler, } from 'expo-notifications' -import { Platform } from 'react-native' -import { navigateToNotification } from '../../lib/deep-linking' -import { - notificationCategories, - notificationsStore, -} from '../../stores/notifications-store' - -type NotificationContent = { - title: string | null - subtitle: string | null - body: string | null - data: { - [key: string]: unknown - } - sound: 'default' | 'defaultCritical' | 'custom' | null - launchImageName: string | null - badge: number | null - attachments: Array<{ - identifier: string | null - url: string | null - type: string | null - }> - summaryArgument?: string | null - summaryArgumentCount?: number - categoryIdentifier: string | null - threadIdentifier: string | null - targetContentIdentifier?: string - color?: string - vibrationPattern?: number[] -} +import { navigateTo, navigateToNotification } from '../../lib/deep-linking' +import { isIos } from '../devices' -export function handleNotificationResponse(response: NotificationResponse) { - // parse notification response and add to the store - const notification = notificationsStore - .getState() - .actions.handleNotificationResponse(response) +export const ACTION_IDENTIFIER_NO_OPERATION = 'NOOP' - // handle notification - const id = response.notification.request.identifier - const content = response.notification.request.content as NotificationContent - const link = notification?.data?.url +export async function handleNotificationResponse({ + actionIdentifier, + notification, +}: NotificationResponse) { + const link = notification.request.content.data?.link - if (response.actionIdentifier === DEFAULT_ACTION_IDENTIFIER) { - navigateToNotification({ id, link }) + if ( + typeof link === 'string' && + actionIdentifier !== ACTION_IDENTIFIER_NO_OPERATION + ) { + navigateToNotification({ link }) } else { - const category = notificationCategories.find( - ({ categoryIdentifier }) => - categoryIdentifier === content.categoryIdentifier, - ) - const action = category?.actions.find( - (x) => x.identifier === response.actionIdentifier, - ) - - if (!category || !action) { - return - } - - // follow the action! - action.onPress(notification) + navigateTo('/notifications') } } @@ -96,14 +54,7 @@ function mapRemoteMessage( } } -export async function setupNotifications() { - // set notification groups - Promise.all( - notificationCategories.map(({ categoryIdentifier, actions }) => { - return setNotificationCategoryAsync(categoryIdentifier, actions) - }), - ) - +export function setupNotifications() { setNotificationHandler({ handleNotification: async () => ({ shouldShowAlert: true, @@ -112,54 +63,52 @@ export async function setupNotifications() { }), }) - addNotificationReceivedListener((notification) => { + addNotificationReceivedListener((notification) => handleNotificationResponse({ notification, - actionIdentifier: 'NOOP', - }) - }) + actionIdentifier: ACTION_IDENTIFIER_NO_OPERATION, + }), + ) - addNotificationResponseReceivedListener((response) => { - handleNotificationResponse(response) - }) + addNotificationResponseReceivedListener((response) => + handleNotificationResponse(response), + ) // FCMs - if (Platform.OS !== 'ios') { - messaging().onNotificationOpenedApp((remoteMessage) => { + if (!isIos) { + messaging().onNotificationOpenedApp((remoteMessage) => handleNotificationResponse({ notification: mapRemoteMessage(remoteMessage), actionIdentifier: DEFAULT_ACTION_IDENTIFIER, - }) - }) + }), + ) - messaging().onMessage(async (remoteMessage) => { + messaging().onMessage((remoteMessage) => handleNotificationResponse({ notification: mapRemoteMessage(remoteMessage), - actionIdentifier: 'NOOP', - }) - }) + actionIdentifier: ACTION_IDENTIFIER_NO_OPERATION, + }), + ) - messaging().setBackgroundMessageHandler(async (remoteMessage) => { + messaging().setBackgroundMessageHandler((remoteMessage) => handleNotificationResponse({ notification: mapRemoteMessage(remoteMessage), - actionIdentifier: 'NOOP', - }) - }) + actionIdentifier: ACTION_IDENTIFIER_NO_OPERATION, + }), + ) } } -export function openInitialNotification() { +export function handleInitialNotificationAndroid() { // FCMs - if (Platform.OS !== 'ios') { - messaging() - .getInitialNotification() - .then((remoteMessage) => { - if (remoteMessage) { - handleNotificationResponse({ - notification: mapRemoteMessage(remoteMessage), - actionIdentifier: DEFAULT_ACTION_IDENTIFIER, - }) - } - }) - } + messaging() + .getInitialNotification() + .then((remoteMessage) => { + if (remoteMessage) { + void handleNotificationResponse({ + notification: mapRemoteMessage(remoteMessage), + actionIdentifier: DEFAULT_ACTION_IDENTIFIER, + }) + } + }) } diff --git a/apps/native/app/src/utils/lifecycle/setup-routes.ts b/apps/native/app/src/utils/lifecycle/setup-routes.ts index 641b23c43415..57d9d0617b7b 100644 --- a/apps/native/app/src/utils/lifecycle/setup-routes.ts +++ b/apps/native/app/src/utils/lifecycle/setup-routes.ts @@ -279,24 +279,10 @@ export function setupRoutes() { }) }) - addRoute('/notification/:id', (passProps) => { - Navigation.showModal({ - stack: { - children: [ - { - component: { - name: ComponentRegistry.NotificationDetailScreen, - passProps, - }, - }, - ], - }, - }) - }) - addRoute( '/wallet/:passId', async ({ passId, fromId, toId, item, ...rest }: any) => { + await Navigation.dismissAllModals() selectTab(1) await Navigation.popToRoot(StackRegistry.WalletStack) Navigation.push(StackRegistry.WalletStack, { @@ -316,6 +302,7 @@ export function setupRoutes() { '/walletpassport/:passId', async ({ passId, fromId, toId, ...rest }: any) => { selectTab(1) + await Navigation.dismissAllModals() await Navigation.popToRoot(StackRegistry.WalletStack) Navigation.push(StackRegistry.WalletStack, { component: { diff --git a/apps/services/auth/admin-api/src/app/v2/scopes/test/me-scopes.spec.ts b/apps/services/auth/admin-api/src/app/v2/scopes/test/me-scopes.spec.ts index 2aba9d0457f2..799cd7c5a026 100644 --- a/apps/services/auth/admin-api/src/app/v2/scopes/test/me-scopes.spec.ts +++ b/apps/services/auth/admin-api/src/app/v2/scopes/test/me-scopes.spec.ts @@ -9,9 +9,14 @@ import { ApiScopeUserClaim, SequelizeConfigService, TranslatedValueDto, + ApiScopeDelegationType, + AdminPatchScopeDto, } from '@island.is/auth-api-lib' import { FixtureFactory } from '@island.is/services/auth/testing' -import { AuthDelegationType } from '@island.is/shared/types' +import { + AuthDelegationProvider, + AuthDelegationType, +} from '@island.is/shared/types' import { isDefined } from '@island.is/shared/utils' import { createCurrentUser, @@ -104,6 +109,29 @@ const createTestData = async ({ ), ) } + + await Promise.all( + [ + [AuthDelegationType.Custom, AuthDelegationProvider.Custom], + [ + AuthDelegationType.ProcurationHolder, + AuthDelegationProvider.CompanyRegistry, + ], + [ + AuthDelegationType.PersonalRepresentative, + AuthDelegationProvider.PersonalRepresentativeRegistry, + ], + [ + AuthDelegationType.LegalGuardian, + AuthDelegationProvider.NationalRegistry, + ], + ].map(async ([delegationType, provider]) => + fixtureFactory.createDelegationType({ + id: delegationType, + providerId: provider, + }), + ), + ) } interface GetTestCase { @@ -240,6 +268,7 @@ const createInput = { const expectedCreateOutput = { ...mockedCreateApiScope, ...createInput, + supportedDelegationTypes: [], } const createTestCases: Record = { @@ -276,6 +305,7 @@ const createTestCases: Record = { ...expectedCreateOutput, grantToAuthenticatedUser: true, grantToLegalGuardians: true, + supportedDelegationTypes: [AuthDelegationType.LegalGuardian], }, }, }, @@ -397,6 +427,7 @@ const patchExpectedOutput = { order: 0, required: false, showInDiscoveryDocument: true, + supportedDelegationTypes: [], ...inputPatch, } @@ -435,6 +466,7 @@ const patchTestCases: Record = { grantToProcuringHolders: false, allowExplicitDelegationGrant: false, isAccessControlled: false, + supportedDelegationTypes: [], }, }, }, @@ -445,7 +477,14 @@ const patchTestCases: Record = { input: inputPatch, expected: { status: 200, - body: patchExpectedOutput, + body: { + ...patchExpectedOutput, + supportedDelegationTypes: [ + AuthDelegationType.Custom, + AuthDelegationType.LegalGuardian, + AuthDelegationType.ProcurationHolder, + ], + }, }, }, 'should return a bad request because of invalid input': { @@ -596,6 +635,7 @@ describe('MeScopesController', () => { const testCase = createTestCases[testCaseName] let app: TestApp let server: request.SuperTest + let apiScopeDelegationTypeModel: typeof ApiScopeDelegationType beforeAll(async () => { app = await setupApp({ @@ -605,6 +645,9 @@ describe('MeScopesController', () => { dbType: 'postgres', }) server = request(app.getHttpServer()) + apiScopeDelegationTypeModel = await app.get( + getModelToken(ApiScopeDelegationType), + ) await createTestData({ app, @@ -655,6 +698,17 @@ describe('MeScopesController', () => { const dbApiScopeUserClaim = await apiScopeUserClaim.findByPk( response.body.name, ) + const apiScopeDelegationTypes = + await apiScopeDelegationTypeModel.findAll({ + where: { + apiScopeName: response.body.name, + }, + }) + + expect(apiScopeDelegationTypes).toHaveLength( + (testCase.expected.body.supportedDelegationTypes as string[]) + .length, + ) expect(dbApiScopeUserClaim).toMatchObject({ apiScopeName: testCase.expected.body.name, @@ -672,6 +726,7 @@ describe('MeScopesController', () => { const testCase = patchTestCases[testCaseName] let app: TestApp let server: request.SuperTest + let apiScopeDelegationTypeModel: typeof ApiScopeDelegationType beforeAll(async () => { app = await setupApp({ @@ -682,6 +737,10 @@ describe('MeScopesController', () => { }) server = request(app.getHttpServer()) + apiScopeDelegationTypeModel = await app.get( + getModelToken(ApiScopeDelegationType), + ) + await createTestData({ app, tenantId: testCase.tenantId, @@ -702,6 +761,274 @@ describe('MeScopesController', () => { // Assert response expect(response.status).toEqual(testCase.expected.status) expect(response.body).toEqual(testCase.expected.body) + + // Assert - db record + if (testCase.expected.body.supportedDelegationTypes) { + const apiScopeDelegationTypes = + await apiScopeDelegationTypeModel.findAll({ + where: { + apiScopeName: testCase.scopeName, + }, + }) + + expect(apiScopeDelegationTypes).toHaveLength( + (testCase.expected.body.supportedDelegationTypes as string[]) + .length, + ) + } + }) + }) + }) + + describe('PATCH: /v2/me/tenants/:tenantId/scopes/:scopeName', () => { + let app: TestApp + let server: request.SuperTest + let apiScopeDelegationTypeModel: typeof ApiScopeDelegationType + + beforeAll(async () => { + app = await setupApp({ + AppModule, + SequelizeConfigService, + user: superUser, + dbType: 'postgres', + }) + server = request(app.getHttpServer()) + + apiScopeDelegationTypeModel = await app.get( + getModelToken(ApiScopeDelegationType), + ) + + await createTestData({ + app, + tenantId: TENANT_ID, + tenantOwnerNationalId: superUser.nationalId, + }) + }) + + const patchAndAssert = async ({ + input, + expected, + }: { + input: AdminPatchScopeDto + expected: Partial + }) => { + const response = await server + .patch( + `/v2/me/tenants/${TENANT_ID}/scopes/${encodeURIComponent( + mockedPatchApiScope.name, + )}`, + ) + .send(input) + + expect(response.status).toEqual(200) + expect(response.body).toMatchObject({ + ...expected, + supportedDelegationTypes: expect.arrayContaining( + expected?.supportedDelegationTypes || [], + ), + }) + const apiScopeDelegationTypes = await apiScopeDelegationTypeModel.findAll( + { + where: { + apiScopeName: mockedPatchApiScope.name, + }, + }, + ) + + expect(apiScopeDelegationTypes).toHaveLength( + expected.supportedDelegationTypes?.length || 0, + ) + } + + it('should delete rows from api_scope_delegation_types table when removing types with boolean fields', async () => { + // add delegation types that we can then remove + await patchAndAssert({ + input: { + grantToPersonalRepresentatives: true, + grantToLegalGuardians: true, + grantToProcuringHolders: true, + allowExplicitDelegationGrant: true, + }, + expected: { + grantToPersonalRepresentatives: true, + grantToLegalGuardians: true, + grantToProcuringHolders: true, + allowExplicitDelegationGrant: true, + supportedDelegationTypes: [ + AuthDelegationType.Custom, + AuthDelegationType.LegalGuardian, + AuthDelegationType.ProcurationHolder, + AuthDelegationType.PersonalRepresentative, + ], + }, + }) + + await patchAndAssert({ + input: { + grantToPersonalRepresentatives: false, + grantToLegalGuardians: false, + grantToProcuringHolders: false, + allowExplicitDelegationGrant: false, + }, + expected: { + grantToPersonalRepresentatives: false, + grantToLegalGuardians: false, + grantToProcuringHolders: false, + allowExplicitDelegationGrant: false, + supportedDelegationTypes: [], + }, + }) + }) + + it('should be able to add supported delegation types to api scope with array property', async () => { + await patchAndAssert({ + input: { + addedDelegationTypes: [ + AuthDelegationType.Custom, + AuthDelegationType.LegalGuardian, + AuthDelegationType.ProcurationHolder, + AuthDelegationType.PersonalRepresentative, + ], + }, + expected: { + grantToPersonalRepresentatives: true, + grantToLegalGuardians: true, + grantToProcuringHolders: true, + allowExplicitDelegationGrant: true, + supportedDelegationTypes: [ + AuthDelegationType.Custom, + AuthDelegationType.LegalGuardian, + AuthDelegationType.ProcurationHolder, + AuthDelegationType.PersonalRepresentative, + ], + }, + }) + }) + + it('should be able to remove supported delegation types to api scope with array property', async () => { + await patchAndAssert({ + input: { + addedDelegationTypes: [ + AuthDelegationType.Custom, + AuthDelegationType.LegalGuardian, + AuthDelegationType.ProcurationHolder, + AuthDelegationType.PersonalRepresentative, + ], + }, + expected: { + grantToPersonalRepresentatives: true, + grantToLegalGuardians: true, + grantToProcuringHolders: true, + allowExplicitDelegationGrant: true, + supportedDelegationTypes: [ + AuthDelegationType.Custom, + AuthDelegationType.LegalGuardian, + AuthDelegationType.ProcurationHolder, + AuthDelegationType.PersonalRepresentative, + ], + }, + }) + + await patchAndAssert({ + input: { + removedDelegationTypes: [ + AuthDelegationType.Custom, + AuthDelegationType.LegalGuardian, + AuthDelegationType.ProcurationHolder, + AuthDelegationType.PersonalRepresentative, + ], + }, + expected: { + grantToPersonalRepresentatives: false, + grantToLegalGuardians: false, + grantToProcuringHolders: false, + allowExplicitDelegationGrant: false, + supportedDelegationTypes: [], + }, + }) + }) + }) + + describe('POST: /v2/me/tenants/:tenantId/scopes', () => { + let app: TestApp + let server: request.SuperTest + let apiScopeDelegationTypeModel: typeof ApiScopeDelegationType + + beforeAll(async () => { + app = await setupApp({ + AppModule, + SequelizeConfigService, + user: superUser, + dbType: 'postgres', + }) + server = request(app.getHttpServer()) + + apiScopeDelegationTypeModel = await app.get( + getModelToken(ApiScopeDelegationType), + ) + + await createTestData({ + app, + tenantId: TENANT_ID, + tenantOwnerNationalId: superUser.nationalId, + }) + }) + + const createAndAssert = async ({ + input, + expected, + }: { + input: AdminCreateScopeDto + expected: Partial + }) => { + const response = await server + .post(`/v2/me/tenants/${TENANT_ID}/scopes`) + .send(input) + + expect(response.status).toEqual(200) + expect(response.body).toMatchObject({ + ...expected, + supportedDelegationTypes: expect.arrayContaining( + expected?.supportedDelegationTypes || [], + ), + }) + + const apiScopeDelegationTypes = await apiScopeDelegationTypeModel.findAll( + { + where: { + apiScopeName: response.body.name, + }, + }, + ) + + expect(apiScopeDelegationTypes).toHaveLength( + expected.supportedDelegationTypes?.length || 0, + ) + } + + it('should be able to create api scope using supportedDelegationTypes property', async () => { + await createAndAssert({ + input: { + ...createInput, + supportedDelegationTypes: [ + AuthDelegationType.Custom, + AuthDelegationType.LegalGuardian, + AuthDelegationType.ProcurationHolder, + AuthDelegationType.PersonalRepresentative, + ], + }, + expected: { + grantToPersonalRepresentatives: true, + grantToLegalGuardians: true, + grantToProcuringHolders: true, + allowExplicitDelegationGrant: true, + supportedDelegationTypes: [ + AuthDelegationType.Custom, + AuthDelegationType.LegalGuardian, + AuthDelegationType.ProcurationHolder, + AuthDelegationType.PersonalRepresentative, + ], + }, }) }) }) diff --git a/apps/services/user-notification/src/app/modules/notifications/notificationDispatch.service.ts b/apps/services/user-notification/src/app/modules/notifications/notificationDispatch.service.ts index 01a4bd233a28..9506ee6be581 100644 --- a/apps/services/user-notification/src/app/modules/notifications/notificationDispatch.service.ts +++ b/apps/services/user-notification/src/app/modules/notifications/notificationDispatch.service.ts @@ -1,38 +1,16 @@ -import { Injectable, Inject } from '@nestjs/common' +import { + Injectable, + Inject, + BadRequestException, + InternalServerErrorException, +} from '@nestjs/common' import * as firebaseAdmin from 'firebase-admin' import type { Logger } from '@island.is/logging' import { LOGGER_PROVIDER } from '@island.is/logging' import { Notification } from './types' -import { isDefined } from './utils' import { FIREBASE_PROVIDER } from '../../../constants' import { V2UsersApi } from '@island.is/clients/user-profile' -export class PushNotificationError extends Error { - constructor(public readonly firebaseErrors: firebaseAdmin.FirebaseError[]) { - super(firebaseErrors.map((e) => e.message).join('. ')) - } -} - -// invalid/outdated token errors are expected -// https://firebase.google.com/docs/cloud-messaging/manage-tokens#detect-invalid-token-responses-from-the-fcm-backend -const isTokenError = (e: firebaseAdmin.FirebaseError): boolean => { - // NB: if there is an issue with the push token we want to ignore the error. - // If there is an error with any other request parameters we want to scream - // error so we can fix it. - // Firebase responds with invalid-argument for both invalid push tokens - // and any other issues with the request parameters. The error code docs - // (https://firebase.google.com/docs/reference/fcm/rest/v1/ErrorCode) says - // that the server responds with which field is invalid, but the FirebaseError - // contains no such information, which means we have no way to tell the - // difference other than inspecting the error message, which is really not - // ideal since technically it might change at any time without notice. - return ( - (e.code === 'messaging/invalid-argument' && - e.message.includes('not a valid FCM registration token')) || - e.code === 'messaging/registration-token-not-registered' - ) -} - @Injectable() export class NotificationDispatchService { constructor( @@ -50,32 +28,66 @@ export class NotificationDispatchService { nationalId: string messageId: string }): Promise { - const deviceTokensResponse = - await this.userProfileApi.userTokenControllerFindUserDeviceToken({ - xParamNationalId: nationalId, - }) - - const tokens = deviceTokensResponse.map((token) => token.deviceToken) + const tokens = await this.getDeviceTokens(nationalId, messageId) if (tokens.length === 0) { - this.logger.info('No push-notification tokens found for user', { - messageId, - }) return - } else { - this.logger.info( - `Found user push-notification tokens (${tokens.length})`, - { messageId }, - ) } - const multiCastMessage = { - tokens, + this.logger.info(`Notification content for message (${messageId})`, { + messageId, + ...notification, + }) + + for (const token of tokens) { + try { + await this.sendNotificationToToken(notification, token, messageId) + } catch (error) { + await this.handleSendError(error, nationalId, token, messageId) + } + } + } + + private async getDeviceTokens( + nationalId: string, + messageId: string, + ): Promise { + try { + const deviceTokensResponse = + await this.userProfileApi.userTokenControllerFindUserDeviceToken({ + xParamNationalId: nationalId, + }) + const tokens = deviceTokensResponse.map((token) => token.deviceToken) + + if (tokens.length === 0) { + this.logger.info('No push-notification tokens found for user', { + messageId, + }) + } else { + this.logger.info( + `Found user push-notification tokens (${tokens.length})`, + { messageId }, + ) + } + + return tokens + } catch (error) { + this.logger.error('Error fetching device tokens', { error, messageId }) + throw new InternalServerErrorException('Error fetching device tokens') + } + } + + private async sendNotificationToToken( + notification: Notification, + token: string, + messageId: string, + ): Promise { + const message = { + token, notification: { title: notification.title, body: notification.body, }, - ...(notification.category && { apns: { payload: { @@ -96,47 +108,79 @@ export class NotificationDispatchService { }, } - this.logger.info(`Notification content for message (${messageId})`, { + await this.firebase.messaging().send(message) + this.logger.info('Push notification success', { messageId, - ...notification, }) + } - const { responses, successCount } = await this.firebase - .messaging() - .sendMulticast(multiCastMessage) - - const errors = responses - .map((r) => r.error) - .filter(isDefined) - .filter((e) => !isTokenError(e)) - - this.logger.info(`Firebase responses for message (${messageId})`, { - responses, - }) - - // throw if unsuccessful and there are unexpected errors - if (successCount === 0 && errors.length > 0) { - throw new PushNotificationError(errors) - } - - // log otherwise - for (const r of responses) { - if (r.error && isTokenError(r.error)) { - this.logger.info('Invalid/outdated push notification token', { - error: r.error, + private async handleSendError( + error: any, + nationalId: string, + token: string, + messageId: string, + ): Promise { + this.logger.error('Push notification error', { error, messageId }) + switch (error.code) { + case 'messaging/invalid-argument': + case 'messaging/registration-token-not-registered': + case 'messaging/invalid-recipient': + await this.removeInvalidToken(nationalId, token, messageId) + break + case 'messaging/invalid-payload': + case 'messaging/invalid-data-key': + case 'messaging/invalid-options': + this.logger.warn('Invalid message payload or options', { + error, messageId, }) - } else if (r.error) { - this.logger.error('Push notification error', { - error: r.error, + break + case 'messaging/quota-exceeded': + this.logger.error('Quota exceeded for sending messages', { + error, messageId, }) - } else { - this.logger.info(`Push notification success`, { - firebaseMessageId: r.messageId, + break + case 'messaging/server-unavailable': + case 'messaging/unavailable': + this.logger.warn('FCM server unavailable, retrying', { + error, messageId, }) - } + break + case 'messaging/message-rate-exceeded': + throw new BadRequestException(error.code) + case 'auth/invalid-credential': + throw new InternalServerErrorException(error.code) + case 'messaging/too-many-messages': + this.logger.warn('Too many messages sent to the device', { + error, + messageId, + }) + break + case 'internal-error': + case 'messaging/unknown-error': + default: + throw new InternalServerErrorException(error.code) + } + } + + private async removeInvalidToken( + nationalId: string, + token: string, + messageId: string, + ): Promise { + try { + await this.userProfileApi.userTokenControllerDeleteUserDeviceToken({ + xParamNationalId: nationalId, + deviceToken: token, + }) + this.logger.info('Removed invalid device token', { token, messageId }) + } catch (error) { + this.logger.error('Error removing device token for user', { + error, + messageId, + }) } } } diff --git a/apps/services/user-profile/src/app/v2/userToken.controller.ts b/apps/services/user-profile/src/app/v2/userToken.controller.ts index d4c8b5d149e4..bc2dc1854de4 100644 --- a/apps/services/user-profile/src/app/v2/userToken.controller.ts +++ b/apps/services/user-profile/src/app/v2/userToken.controller.ts @@ -1,5 +1,12 @@ import { ApiSecurity, ApiTags } from '@nestjs/swagger' -import { Controller, Get, UseGuards, Headers } from '@nestjs/common' +import { + Controller, + Get, + UseGuards, + Headers, + Delete, + Param, +} from '@nestjs/common' import { IdsAuthGuard, Scopes, ScopesGuard } from '@island.is/auth-nest-tools' import { UserProfileScope } from '@island.is/auth/scopes' @@ -44,4 +51,22 @@ export class UserTokenController { ): Promise { return this.userTokenService.findAllUserTokensByNationalId(nationalId) } + + @Delete(':deviceToken') + @Documentation({ + description: 'Delete a user device token.', + response: { status: 204 }, + }) + @Audit({ + resources: (deviceToken: string) => deviceToken, + }) + async deleteUserDeviceToken( + @Headers('X-Param-National-Id') nationalId: string, + @Param('deviceToken') deviceToken: string, + ): Promise { + await this.userTokenService.deleteUserTokenByNationalId( + nationalId, + deviceToken, + ) + } } diff --git a/apps/services/user-profile/src/app/v2/userToken.service.ts b/apps/services/user-profile/src/app/v2/userToken.service.ts index daaf93623edb..5ddbf45abfab 100644 --- a/apps/services/user-profile/src/app/v2/userToken.service.ts +++ b/apps/services/user-profile/src/app/v2/userToken.service.ts @@ -17,4 +17,16 @@ export class UserTokenService { order: [['created', 'DESC']], }) } + + async deleteUserTokenByNationalId( + nationalId: string, + deviceToken: string, + ): Promise { + await this.userDeviceTokensModel.destroy({ + where: { + nationalId, + deviceToken, + }, + }) + } } diff --git a/apps/web/components/Charts/v2/renderers/ChartRenderer.tsx b/apps/web/components/Charts/v2/renderers/ChartRenderer.tsx index 46e64826f71e..f17869dd76d6 100644 --- a/apps/web/components/Charts/v2/renderers/ChartRenderer.tsx +++ b/apps/web/components/Charts/v2/renderers/ChartRenderer.tsx @@ -61,8 +61,14 @@ export const Chart = ({ slice }: ChartProps) => { activeLocale, slice.xAxisValueType || DEFAULT_XAXIS_VALUE_TYPE, slice.xAxisFormat || undefined, + slice.reduceAndRoundValue || undefined, )(value), - [activeLocale, slice.xAxisValueType, slice.xAxisFormat], + [ + activeLocale, + slice.xAxisValueType, + slice.xAxisFormat, + slice.reduceAndRoundValue, + ], ) const componentsWithAddedProps = useGetChartComponentsWithRenderProps(slice) diff --git a/apps/web/components/Charts/v2/renderers/TooltipRenderer/TooltipRenderer.tsx b/apps/web/components/Charts/v2/renderers/TooltipRenderer/TooltipRenderer.tsx index 29e1d31a3f9b..ab3ce08a1b0b 100644 --- a/apps/web/components/Charts/v2/renderers/TooltipRenderer/TooltipRenderer.tsx +++ b/apps/web/components/Charts/v2/renderers/TooltipRenderer/TooltipRenderer.tsx @@ -63,7 +63,12 @@ export const CustomTooltipRenderer = (props: CustomTooltipProps) => { :{' '} {item.value - ? formatValueForPresentation(activeLocale, item.value, true, 1) + ? formatValueForPresentation( + activeLocale, + item.value, + props.slice.reduceAndRoundValue ?? true, + 1, + ) : ''} ) diff --git a/apps/web/components/Charts/v2/utils/chart.tsx b/apps/web/components/Charts/v2/utils/chart.tsx index ffa5f896ec89..5bb48b49e14d 100644 --- a/apps/web/components/Charts/v2/utils/chart.tsx +++ b/apps/web/components/Charts/v2/utils/chart.tsx @@ -78,7 +78,11 @@ export const getCartesianGridComponents = ({ const xAxisFormatter = tickFormatter const yAxisFormatter = (v: string | number) => - formatValueForPresentation(activeLocale, v) + formatValueForPresentation( + activeLocale, + v, + slice.reduceAndRoundValue ?? true, + ) return [ + ( + activeLocale: Locale, + xAxisValueType?: string, + xAxisFormat?: string, + reduceAndRoundValue?: boolean, + ) => (value: unknown) => { // Date is the default is value type is undefined if (!xAxisValueType || xAxisValueType === 'date') { return formatDate(activeLocale, value as Date, xAxisFormat || undefined) } else if (xAxisValueType === 'number') { - return formatValueForPresentation(activeLocale, value as string | number) + return formatValueForPresentation( + activeLocale, + value as string | number, + reduceAndRoundValue ?? true, + ) } return value as string diff --git a/apps/web/public/.well-known/assetlinks.json b/apps/web/public/.well-known/assetlinks.json index 89d442bcc2ab..bf1a57c49169 100644 --- a/apps/web/public/.well-known/assetlinks.json +++ b/apps/web/public/.well-known/assetlinks.json @@ -1,21 +1,29 @@ [ { - "relation": ["delegate_permission/common.get_login_creds"], + "relation": [ + "delegate_permission/common.handle_all_urls", + "delegate_permission/common.get_login_creds" + ], "target": { "namespace": "android_app", "package_name": "is.island.app", "sha256_cert_fingerprints": [ - "12:C2:D3:52:EE:64:69:8E:D7:3E:63:25:D9:FE:E7:6E:AE:1A:9A:EF:8F:37:37:58:BB:71:5E:70:D7:FD:D3:05" + "12:C2:D3:52:EE:64:69:8E:D7:3E:63:25:D9:FE:E7:6E:AE:1A:9A:EF:8F:37:37:58:BB:71:5E:70:D7:FD:D3:05", + "26:03:DE:A3:F1:7A:29:89:3E:9E:04:5A:DB:AB:4E:D9:2B:00:B4:C8:93:05:00:9C:ED:6B:52:80:DF:A3:45:7D" ] } }, { - "relation": ["delegate_permission/common.get_login_creds"], + "relation": [ + "delegate_permission/common.handle_all_urls", + "delegate_permission/common.get_login_creds" + ], "target": { "namespace": "android_app", "package_name": "is.island.app.dev", "sha256_cert_fingerprints": [ - "C2:B9:0C:C4:3F:7C:22:2E:65:C6:02:5D:AB:B8:5E:06:AF:F1:4E:AD:FC:FE:CF:46:28:B2:E0:11:23:9B:9C:C4" + "C2:B9:0C:C4:3F:7C:22:2E:65:C6:02:5D:AB:B8:5E:06:AF:F1:4E:AD:FC:FE:CF:46:28:B2:E0:11:23:9B:9C:C4", + "26:03:DE:A3:F1:7A:29:89:3E:9E:04:5A:DB:AB:4E:D9:2B:00:B4:C8:93:05:00:9C:ED:6B:52:80:DF:A3:45:7D" ] } } diff --git a/apps/web/screens/queries/fragments.ts b/apps/web/screens/queries/fragments.ts index 2ac52696adeb..569c8104e62d 100644 --- a/apps/web/screens/queries/fragments.ts +++ b/apps/web/screens/queries/fragments.ts @@ -833,6 +833,7 @@ export const slices = gql` xAxisFormat xAxisValueType customStyleConfig + reduceAndRoundValue } fragment ChartNumberBoxFields on ChartNumberBox { diff --git a/charts/islandis/values.dev.yaml b/charts/islandis/values.dev.yaml index f4ebc10fe92f..0cb229836f8d 100644 --- a/charts/islandis/values.dev.yaml +++ b/charts/islandis/values.dev.yaml @@ -453,10 +453,10 @@ api: min: 2 resources: limits: - cpu: '600m' + cpu: '1200m' memory: '2048Mi' requests: - cpu: '250m' + cpu: '350m' memory: '896Mi' secrets: ADR_LICENSE_FETCH_TIMEOUT: '/k8s/api/ADR_LICENSE_FETCH_TIMEOUT' diff --git a/charts/islandis/values.prod.yaml b/charts/islandis/values.prod.yaml index 4c055e99c61f..d0fec583b383 100644 --- a/charts/islandis/values.prod.yaml +++ b/charts/islandis/values.prod.yaml @@ -443,10 +443,10 @@ api: min: 2 resources: limits: - cpu: '600m' + cpu: '1200m' memory: '2048Mi' requests: - cpu: '250m' + cpu: '350m' memory: '896Mi' secrets: ADR_LICENSE_FETCH_TIMEOUT: '/k8s/api/ADR_LICENSE_FETCH_TIMEOUT' diff --git a/charts/islandis/values.staging.yaml b/charts/islandis/values.staging.yaml index b0471e7f0b99..539c6063feaa 100644 --- a/charts/islandis/values.staging.yaml +++ b/charts/islandis/values.staging.yaml @@ -451,10 +451,10 @@ api: min: 2 resources: limits: - cpu: '600m' + cpu: '1200m' memory: '2048Mi' requests: - cpu: '250m' + cpu: '350m' memory: '896Mi' secrets: ADR_LICENSE_FETCH_TIMEOUT: '/k8s/api/ADR_LICENSE_FETCH_TIMEOUT' diff --git a/libs/api/domains/notifications/src/lib/notifications.service.ts b/libs/api/domains/notifications/src/lib/notifications.service.ts index fe1a7b156a6a..97ea8730e91e 100644 --- a/libs/api/domains/notifications/src/lib/notifications.service.ts +++ b/libs/api/domains/notifications/src/lib/notifications.service.ts @@ -136,6 +136,7 @@ export class NotificationsService { id, updateNotificationDto: { read: true, + seen: true, }, }) diff --git a/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/aboutTheAccidentSection.ts b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/aboutTheAccidentSection.ts deleted file mode 100644 index ccf45395f1b9..000000000000 --- a/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/aboutTheAccidentSection.ts +++ /dev/null @@ -1,1420 +0,0 @@ -import { - buildAlertMessageField, - buildCustomField, - buildDescriptionField, - buildFileUploadField, - buildMultiField, - buildRadioField, - buildSection, - buildSubSection, - buildTextField, - getValueViaPath, -} from '@island.is/application/core' -import { NO, UPLOAD_ACCEPT, YES, FILE_SIZE_LIMIT } from '../../constants' -import { - accidentDetails, - accidentLocation, - accidentType, - application, - companyInfo, - fatalAccident, - fatalAccidentAttachment, - fishingCompanyInfo, - injuredPersonInformation, - locationAndPurpose, - rescueSquadInfo, - schoolInfo, - sportsClubInfo, - workMachine, - representativeInfo, - addDocuments, - attachments, - error, -} from '../../lib/messages' -import { - AgricultureAccidentLocationEnum, - AttachmentsEnum, - FishermanWorkplaceAccidentLocationEnum, - FishermanWorkplaceAccidentShipLocationEnum, - GeneralWorkplaceAccidentLocationEnum, - ProfessionalAthleteAccidentLocationEnum, - RescueWorkAccidentLocationEnum, - StudiesAccidentLocationEnum, - StudiesAccidentTypeEnum, - WorkAccidentTypeEnum, -} from '../../types' -import { - getAccidentTypeOptions, - hideLocationAndPurpose, - isAgricultureAccident, - isDateOlderThanAYear, - isFatalAccident, - isFishermanAccident, - isGeneralWorkplaceAccident, - isHomeActivitiesAccident, - isInjuredAndRepresentativeOfCompanyOrInstitute, - isInternshipStudiesAccident, - isProfessionalAthleteAccident, - isReportingOnBehalfOfEmployee, - isReportingOnBehalfOfInjured, - isReportingOnBehalfSelf, - isRepresentativeOfCompanyOrInstitute, - isRescueWorkAccident, - isStudiesAccident, - isWorkAccident, -} from '../../utils' -import { isHealthInsured } from '../../utils/isHealthInsured' -import { isSportAccidentAndEmployee } from '../../utils/isSportAccidentAndEmployee' - -export const aboutTheAccidentSection = buildSection({ - id: 'accidentType.section', - title: accidentType.general.sectionTitle, - children: [ - buildSubSection({ - id: 'accidentType.section', - title: accidentType.general.subsectionTitle, - children: [ - buildRadioField({ - id: 'accidentType.radioButton', - width: 'half', - title: accidentType.general.heading, - description: accidentType.general.description, - options: (formValue) => getAccidentTypeOptions(formValue.answers), - }), - ], - }), - buildSubSection({ - id: 'workAccident.subSection', - title: accidentType.workAccidentType.subSectionTitle, - condition: (formValue) => isWorkAccident(formValue), - children: [ - buildMultiField({ - id: 'workAccident.section', - title: accidentType.workAccidentType.heading, - description: accidentType.workAccidentType.description, - children: [ - buildRadioField({ - id: 'workAccident.type', - width: 'half', - title: '', - options: [ - { - value: WorkAccidentTypeEnum.GENERAL, - label: accidentType.workAccidentType.generalWorkAccident, - }, - { - value: WorkAccidentTypeEnum.FISHERMAN, - label: accidentType.workAccidentType.fishermanAccident, - }, - { - value: WorkAccidentTypeEnum.PROFESSIONALATHLETE, - label: accidentType.workAccidentType.professionalAthlete, - }, - { - value: WorkAccidentTypeEnum.AGRICULTURE, - label: accidentType.workAccidentType.agricultureAccident, - }, - ], - }), - buildAlertMessageField({ - id: 'attachments.injuryCertificate.alert2', - title: attachments.labels.alertMessage, - description: accidentType.warning.agricultureAccidentWarning, - doesNotRequireAnswer: true, - message: accidentType.warning.agricultureAccidentWarning, - alertType: 'warning', - condition: (formValue) => isAgricultureAccident(formValue), - marginBottom: 5, - }), - buildDescriptionField({ - id: 'accidentDetails.descriptionField', - space: 'containerGutter', - title: injuredPersonInformation.general.jobTitle, - description: injuredPersonInformation.general.jobTitleDescription, - width: 'full', - marginBottom: 2, - condition: (formValue) => isReportingOnBehalfSelf(formValue), - }), - buildTextField({ - id: 'injuredPersonInformation.jobTitle', - title: injuredPersonInformation.labels.jobTitle, - backgroundColor: 'white', - width: 'full', - maxLength: 100, - condition: (formValue) => isReportingOnBehalfSelf(formValue), - }), - ], - }), - ], - }), - buildSubSection({ - id: 'studiesAccident.subSection', - title: accidentType.workAccidentType.subSectionTitle, - condition: (formValue) => isStudiesAccident(formValue), - children: [ - buildMultiField({ - id: 'studiesAccident.section', - title: accidentType.studiesAccidentType.heading, - description: accidentType.studiesAccidentType.description, - children: [ - buildRadioField({ - id: 'studiesAccident.type', - title: '', - options: [ - { - value: StudiesAccidentTypeEnum.INTERNSHIP, - label: accidentType.studiesAccidentType.internship, - }, - { - value: StudiesAccidentTypeEnum.APPRENTICESHIP, - label: accidentType.studiesAccidentType.apprenticeship, - }, - { - value: StudiesAccidentTypeEnum.VOCATIONALEDUCATION, - label: accidentType.studiesAccidentType.vocationalEducation, - }, - ], - }), - ], - }), - ], - }), - // Location Subsection - buildSubSection({ - id: 'location.subSection', - title: 'Staðsetning', - children: [ - buildMultiField({ - id: 'sportsClubInfo.employee.field', - title: sportsClubInfo.employee.title, - condition: (formValue) => isProfessionalAthleteAccident(formValue), - children: [ - buildRadioField({ - id: 'onPayRoll.answer', - width: 'half', - title: '', - options: [ - { - value: YES, - label: application.general.yesOptionLabel, - }, - { - value: NO, - label: application.general.noOptionLabel, - }, - ], - }), - buildAlertMessageField({ - id: 'attachments.injuryCertificate.alert', - title: application.labels.warningTitle, - message: application.labels.warningMessage, - alertType: 'info', - doesNotRequireAnswer: true, - condition: (formValue) => isSportAccidentAndEmployee(formValue), - }), - ], - }), - - // Accident location section - // location of home related accident - buildMultiField({ - id: 'accidentLocation.homeAccident', - title: accidentLocation.homeAccidentLocation.title, - description: accidentLocation.homeAccidentLocation.description, - condition: (formValue) => isHomeActivitiesAccident(formValue), - children: [ - buildTextField({ - id: 'homeAccident.address', - title: accidentLocation.homeAccidentLocation.address, - backgroundColor: 'blue', - required: true, - maxLength: 100, - }), - buildTextField({ - id: 'homeAccident.postalCode', - title: accidentLocation.homeAccidentLocation.postalCode, - backgroundColor: 'blue', - width: 'half', - format: '###', - required: true, - }), - buildTextField({ - id: 'homeAccident.community', - title: accidentLocation.homeAccidentLocation.community, - backgroundColor: 'blue', - width: 'half', - required: true, - maxLength: 100, - }), - buildTextField({ - id: 'homeAccident.moreDetails', - title: accidentLocation.homeAccidentLocation.moreDetails, - placeholder: - accidentLocation.homeAccidentLocation.moreDetailsPlaceholder, - backgroundColor: 'blue', - rows: 4, - variant: 'textarea', - maxLength: 2000, - }), - ], - }), - // location of general work related accident - buildMultiField({ - id: 'accidentLocation.generalWorkAccident', - title: accidentLocation.general.heading, - description: accidentLocation.general.description, - condition: (formValue) => - isGeneralWorkplaceAccident(formValue) || - isSportAccidentAndEmployee(formValue), - children: [ - buildRadioField({ - id: 'accidentLocation.answer', - title: '', - options: [ - { - value: GeneralWorkplaceAccidentLocationEnum.ATTHEWORKPLACE, - label: accidentLocation.generalWorkAccident.atTheWorkplace, - }, - { - value: - GeneralWorkplaceAccidentLocationEnum.TOORFROMTHEWORKPLACE, - label: - accidentLocation.generalWorkAccident.toOrFromTheWorkplace, - }, - { - value: GeneralWorkplaceAccidentLocationEnum.OTHER, - label: accidentLocation.generalWorkAccident.other, - }, - ], - }), - ], - }), - // location of rescue work related accident - buildMultiField({ - id: 'accidentLocation.rescueWorkAccident', - title: accidentLocation.general.heading, - description: accidentLocation.rescueWorkAccident.description, - condition: (formValue) => { - return isRescueWorkAccident(formValue) - }, - children: [ - buildRadioField({ - id: 'accidentLocation.answer', - title: '', - options: [ - { - value: RescueWorkAccidentLocationEnum.DURINGRESCUE, - label: accidentLocation.rescueWorkAccident.duringRescue, - }, - { - value: RescueWorkAccidentLocationEnum.TOORFROMRESCUE, - label: accidentLocation.rescueWorkAccident.toOrFromRescue, - }, - { - value: RescueWorkAccidentLocationEnum.OTHER, - label: accidentLocation.rescueWorkAccident.other, - }, - ], - }), - ], - }), - // location of studies related accident - buildMultiField({ - id: 'accidentLocation.studiesAccident', - title: accidentLocation.studiesAccidentLocation.heading, - description: accidentLocation.studiesAccidentLocation.description, - condition: (formValue) => - isStudiesAccident(formValue) && - !isInternshipStudiesAccident(formValue), - children: [ - buildRadioField({ - id: 'accidentLocation.answer', - title: '', - options: [ - { - value: StudiesAccidentLocationEnum.ATTHESCHOOL, - label: accidentLocation.studiesAccidentLocation.atTheSchool, - }, - { - value: StudiesAccidentLocationEnum.OTHER, - label: accidentLocation.studiesAccidentLocation.other, - }, - ], - }), - ], - }), - // location of fisherman related accident - buildMultiField({ - id: 'accidentLocation.fishermanAccident', - title: accidentLocation.general.heading, - description: accidentLocation.general.description, - condition: (formValue) => isFishermanAccident(formValue), - children: [ - buildRadioField({ - id: 'accidentLocation.answer', - title: '', - options: [ - { - value: FishermanWorkplaceAccidentLocationEnum.ONTHESHIP, - label: accidentLocation.fishermanAccident.onTheShip, - }, - { - value: - FishermanWorkplaceAccidentLocationEnum.TOORFROMTHEWORKPLACE, - label: - accidentLocation.fishermanAccident.toOrFromTheWorkplace, - }, - { - value: FishermanWorkplaceAccidentLocationEnum.OTHER, - label: accidentLocation.fishermanAccident.other, - }, - ], - }), - ], - }), - // location of sports related accident - buildMultiField({ - id: 'accidentLocation.professionalAthleteAccident', - title: accidentLocation.general.heading, - description: accidentLocation.general.description, - condition: (formValue) => - isProfessionalAthleteAccident(formValue) && - !isSportAccidentAndEmployee(formValue), - children: [ - buildRadioField({ - id: 'accidentLocation.answer', - title: '', - options: [ - { - value: - ProfessionalAthleteAccidentLocationEnum.SPORTCLUBSFACILITES, - label: - accidentLocation.professionalAthleteAccident - .atTheClubsSportsFacilites, - }, - { - value: - ProfessionalAthleteAccidentLocationEnum.TOORFROMTHESPORTCLUBSFACILITES, - label: - accidentLocation.professionalAthleteAccident - .toOrFromTheSportsFacilites, - }, - { - value: ProfessionalAthleteAccidentLocationEnum.OTHER, - label: accidentLocation.professionalAthleteAccident.other, - }, - ], - }), - ], - }), - // location of agriculture related accident - buildMultiField({ - id: 'accidentLocation.agricultureAccident', - title: accidentLocation.general.heading, - description: accidentLocation.general.description, - condition: (formValue) => isAgricultureAccident(formValue), - children: [ - buildRadioField({ - id: 'accidentLocation.answer', - title: '', - options: [ - { - value: AgricultureAccidentLocationEnum.ATTHEWORKPLACE, - label: accidentLocation.agricultureAccident.atTheWorkplace, - }, - { - value: AgricultureAccidentLocationEnum.TOORFROMTHEWORKPLACE, - label: - accidentLocation.agricultureAccident.toOrFromTheWorkplace, - }, - { - value: AgricultureAccidentLocationEnum.OTHER, - label: accidentLocation.agricultureAccident.other, - }, - ], - }), - ], - }), - // Fisherman information only applicable to fisherman workplace accidents - buildMultiField({ - id: 'shipLocation.multifield', - title: accidentLocation.fishermanAccidentLocation.heading, - description: accidentLocation.fishermanAccidentLocation.description, - condition: (formValue) => isFishermanAccident(formValue), - children: [ - buildRadioField({ - id: 'shipLocation.answer', - title: '', - backgroundColor: 'blue', - options: [ - { - value: - FishermanWorkplaceAccidentShipLocationEnum.SAILINGORFISHING, - label: - accidentLocation.fishermanAccidentLocation.whileSailing, - }, - { - value: FishermanWorkplaceAccidentShipLocationEnum.HARBOR, - label: accidentLocation.fishermanAccidentLocation.inTheHarbor, - }, - { - value: FishermanWorkplaceAccidentShipLocationEnum.OTHER, - label: accidentLocation.fishermanAccidentLocation.other, - }, - ], - }), - ], - }), - buildMultiField({ - id: 'locationAndPurpose', - title: locationAndPurpose.general.title, - description: locationAndPurpose.general.description, - condition: (formValue) => - !isFishermanAccident(formValue) && - !hideLocationAndPurpose(formValue), - children: [ - buildTextField({ - id: 'locationAndPurpose.location', - title: locationAndPurpose.labels.location, - backgroundColor: 'blue', - variant: 'textarea', - required: true, - rows: 4, - maxLength: 2000, - }), - ], - }), - ], - }), - // Workmachine information only applicable to generic workplace accidents - buildSubSection({ - id: 'workMachine.section', - title: workMachine.general.sectionTitle, - condition: (formValue) => - isGeneralWorkplaceAccident(formValue) || - isAgricultureAccident(formValue) || - isSportAccidentAndEmployee(formValue), - children: [ - buildMultiField({ - id: 'workMachine', - title: workMachine.general.workMachineRadioTitle, - description: '', - children: [ - buildRadioField({ - id: 'workMachineRadio', - title: '', - backgroundColor: 'blue', - width: 'half', - required: true, - options: [ - { value: YES, label: application.general.yesOptionLabel }, - { value: NO, label: application.general.noOptionLabel }, - ], - }), - ], - }), - buildMultiField({ - id: 'workMachine.description', - title: workMachine.general.subSectionTitle, - condition: (formValue) => formValue.workMachineRadio === YES, - children: [ - buildTextField({ - id: 'workMachine.desriptionOfMachine', - title: workMachine.labels.desriptionOfMachine, - placeholder: workMachine.placeholder.desriptionOfMachine, - backgroundColor: 'blue', - rows: 4, - variant: 'textarea', - required: true, - maxLength: 2000, - }), - ], - }), - ], - }), - // Details of the accident - buildSubSection({ - id: 'accidentDetails.section', - title: accidentDetails.general.sectionTitle, - children: [ - buildMultiField({ - id: 'accidentDetails', - title: accidentDetails.general.sectionTitle, - description: accidentDetails.general.description, - children: [ - buildCustomField({ - id: 'accidentDetails.dateOfAccident', - title: accidentDetails.labels.date, - component: 'DateOfAccident', - width: 'half', - }), - buildTextField({ - id: 'accidentDetails.timeOfAccident', - title: accidentDetails.labels.time, - placeholder: accidentDetails.placeholder.time, - backgroundColor: 'blue', - required: true, - width: 'half', - format: '##:##', - }), - buildAlertMessageField({ - id: 'accidentDetails.moreThanAYearAlertMessage', - title: accidentDetails.general.moreThanAYearAlertTitle, - message: accidentDetails.general.moreThanAYearAlertMessage, - width: 'full', - alertType: 'warning', - condition: (formValue) => isDateOlderThanAYear(formValue), - marginBottom: 0, - }), - buildAlertMessageField({ - id: 'accidentDetails.notHealthInsuredAlertMessage', - title: accidentDetails.general.insuranceAlertTitle, - message: accidentDetails.general.insuranceAlertText, - width: 'full', - alertType: 'warning', - condition: (formValue) => !isHealthInsured(formValue), - marginBottom: 0, - }), - buildTextField({ - id: 'accidentDetails.descriptionOfAccident', - title: accidentDetails.labels.description, - placeholder: accidentDetails.placeholder.description, - backgroundColor: 'blue', - required: true, - rows: 10, - variant: 'textarea', - maxLength: 2000, - }), - buildTextField({ - id: 'accidentDetails.accidentSymptoms', - title: accidentDetails.labels.symptoms, - placeholder: accidentDetails.placeholder.symptoms, - backgroundColor: 'blue', - required: true, - rows: 10, - variant: 'textarea', - maxLength: 2000, - }), - buildDescriptionField({ - id: 'accidentDetails.descriptionField', - space: 'containerGutter', - titleVariant: 'h5', - title: accidentDetails.labels.doctorVisit, - width: 'full', - }), - buildCustomField({ - id: 'accidentDetails.dateOfDoctorVisit', - title: accidentDetails.labels.date, - component: 'DateOfAccident', - width: 'half', - }), - buildTextField({ - id: 'accidentDetails.timeOfDoctorVisit', - title: accidentDetails.labels.time, - placeholder: accidentDetails.placeholder.doctorVisitTime, - backgroundColor: 'blue', - width: 'half', - format: '##:##', - }), - ], - }), - ], - }), - - // Injury Certificate and Fatal accident section - buildSubSection({ - id: 'attachments.section', - title: attachments.general.sectionTitle, - children: [ - buildMultiField({ - id: 'attachments', - title: attachments.general.heading, - children: [ - buildRadioField({ - id: 'injuryCertificate.answer', - title: '', - description: attachments.general.description, - required: true, - options: (application) => - isRepresentativeOfCompanyOrInstitute(application.answers) - ? [ - { - value: AttachmentsEnum.INJURYCERTIFICATE, - label: attachments.labels.injuryCertificate, - }, - { - value: AttachmentsEnum.SENDCERTIFICATELATER, - label: attachments.labels.sendCertificateLater, - }, - ] - : [ - { - value: AttachmentsEnum.INJURYCERTIFICATE, - label: attachments.labels.injuryCertificate, - }, - { - value: AttachmentsEnum.HOSPITALSENDSCERTIFICATE, - label: attachments.labels.hospitalSendsCertificate, - }, - { - value: AttachmentsEnum.SENDCERTIFICATELATER, - label: attachments.labels.sendCertificateLater, - }, - ], - }), - buildAlertMessageField({ - id: 'attachments.injuryCertificate.alert', - title: attachments.labels.alertMessage, - message: attachments.general.alertMessage, - doesNotRequireAnswer: true, - condition: (formValue) => - getValueViaPath(formValue, 'injuryCertificate.answer') === - AttachmentsEnum.SENDCERTIFICATELATER, - alertType: 'warning', - }), - ], - }), - buildMultiField({ - id: 'attachments.injuryCertificateFile.subSection', - title: attachments.general.heading, - children: [ - buildFileUploadField({ - id: 'attachments.injuryCertificateFile.file', - title: attachments.general.heading, - maxSize: FILE_SIZE_LIMIT, - maxSizeErrorText: error.attachmentMaxSizeError, - uploadAccept: UPLOAD_ACCEPT, - uploadHeader: injuredPersonInformation.upload.uploadHeader, - uploadDescription: attachments.general.uploadDescription, - uploadButtonLabel: attachments.general.uploadButtonLabel, - introduction: attachments.general.uploadIntroduction, - }), - ], - condition: (formValue) => - getValueViaPath(formValue, 'injuryCertificate.answer') === - AttachmentsEnum.INJURYCERTIFICATE, - }), - buildMultiField({ - id: 'fatalAccidentMulti.section', - title: fatalAccident.general.sectionTitle, - condition: (formValue) => isReportingOnBehalfOfInjured(formValue), - children: [ - buildRadioField({ - id: 'wasTheAccidentFatal', - title: '', - backgroundColor: 'blue', - width: 'half', - required: true, - options: [ - { value: YES, label: application.general.yesOptionLabel }, - { value: NO, label: application.general.noOptionLabel }, - ], - }), - ], - }), - buildMultiField({ - id: 'fatalAccidentUploadDeathCertificateNowMulti', - title: fatalAccidentAttachment.labels.title, - description: fatalAccidentAttachment.labels.description, - condition: (formValue) => - isReportingOnBehalfOfInjured(formValue) && - formValue.wasTheAccidentFatal === YES, - children: [ - buildRadioField({ - id: 'fatalAccidentUploadDeathCertificateNow', - title: '', - backgroundColor: 'blue', - required: true, - options: [ - { - value: YES, - label: fatalAccidentAttachment.options.addAttachmentsNow, - }, - { - value: NO, - label: fatalAccidentAttachment.options.addAttachmentsLater, - }, - ], - }), - buildAlertMessageField({ - id: 'attachments.injuryCertificate.alert', - title: fatalAccident.alertMessage.title, - message: fatalAccident.alertMessage.description, - doesNotRequireAnswer: true, - alertType: 'warning', - condition: (formValue) => - getValueViaPath( - formValue, - 'fatalAccidentUploadDeathCertificateNow', - ) === NO, - }), - ], - }), - - buildMultiField({ - id: 'attachments.deathCertificateFile.subSection', - title: attachments.general.uploadTitle, - condition: (formValue) => - isReportingOnBehalfOfInjured(formValue) && - isFatalAccident(formValue) && - formValue.fatalAccidentUploadDeathCertificateNow === YES, - children: [ - buildFileUploadField({ - id: 'attachments.deathCertificateFile.file', - title: attachments.general.uploadHeader, - maxSize: FILE_SIZE_LIMIT, - maxSizeErrorText: error.attachmentMaxSizeError, - uploadAccept: UPLOAD_ACCEPT, - uploadHeader: attachments.general.uploadHeader, - uploadDescription: attachments.general.uploadDescription, - uploadButtonLabel: attachments.general.uploadButtonLabel, - introduction: attachments.general.uploadIntroduction, - }), - ], - }), - buildMultiField({ - id: 'attachments.additionalFilesMulti', - title: attachments.general.heading, - children: [ - buildRadioField({ - id: 'additionalAttachments.answer', - title: '', - description: attachments.general.additionalAttachmentDescription, - required: true, - options: () => [ - { - value: AttachmentsEnum.ADDITIONALNOW, - label: attachments.labels.additionalNow, - }, - { - value: AttachmentsEnum.ADDITIONALLATER, - label: attachments.labels.additionalLater, - }, - ], - }), - buildAlertMessageField({ - id: 'attachments.injuryCertificate.alert', - title: attachments.labels.alertMessage, - message: attachments.general.alertMessage, - alertType: 'warning', - doesNotRequireAnswer: true, - condition: (formValue) => - getValueViaPath(formValue, 'additionalAttachments.answer') === - AttachmentsEnum.ADDITIONALLATER, - }), - ], - }), - buildMultiField({ - id: 'attachments.additionalAttachments.subSection', - title: attachments.general.heading, - condition: (formValue) => - getValueViaPath(formValue, 'additionalAttachments.answer') === - AttachmentsEnum.ADDITIONALNOW, - children: [ - buildFileUploadField({ - id: 'attachments.additionalFiles.file', - title: attachments.general.heading, - maxSize: FILE_SIZE_LIMIT, - maxSizeErrorText: error.attachmentMaxSizeError, - uploadAccept: UPLOAD_ACCEPT, - uploadHeader: addDocuments.general.uploadHeader, - uploadDescription: addDocuments.general.uploadDescription, - uploadButtonLabel: addDocuments.general.uploadButtonLabel, - introduction: addDocuments.general.additionalDocumentsDescription, - }), - ], - }), - ], - }), - - // Company information if work accident without the injured being a fisherman or in agriculture - buildSubSection({ - id: 'companyInfo.subSection', - title: companyInfo.general.title, - condition: (formValue) => - !isAgricultureAccident(formValue) && - !isReportingOnBehalfOfEmployee(formValue) && - !isHomeActivitiesAccident(formValue) && - (isGeneralWorkplaceAccident(formValue) || - isInternshipStudiesAccident(formValue)), - children: [ - buildMultiField({ - id: 'companyInfo', - title: companyInfo.general.title, - description: companyInfo.general.description, - children: [ - buildTextField({ - id: 'companyInfo.name', - title: companyInfo.labels.name, - backgroundColor: 'blue', - required: true, - width: 'half', - maxLength: 100, - }), - buildTextField({ - id: 'companyInfo.nationalRegistrationId', - title: companyInfo.labels.nationalId, - backgroundColor: 'blue', - width: 'half', - format: '######-####', - required: true, - }), - // buildCheckboxField({ - // id: 'isRepresentativeOfCompanyOrInstitue', - // title: '', - // defaultValue: [], - // large: false, - // backgroundColor: 'white', - // options: [ - // { - // value: YES, - // label: companyInfo.labels.checkBox, - // }, - // ], - // }), - buildDescriptionField({ - id: 'companyInfo.descriptionField', - description: companyInfo.labels.subDescription, - space: 'containerGutter', - titleVariant: 'h5', - title: companyInfo.labels.descriptionField, - condition: (formValue) => - !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), - }), - - // These should all be required if the user is not the representative of the company. - // Should look into if we can require conditionally - buildTextField({ - id: 'representative.name', - title: representativeInfo.labels.name, - backgroundColor: 'blue', - required: true, - width: 'half', - maxLength: 100, - condition: (formValue) => - !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), - }), - buildTextField({ - id: 'representative.nationalId', - title: representativeInfo.labels.nationalId, - backgroundColor: 'blue', - required: true, - width: 'half', - format: '######-####', - condition: (formValue) => - !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), - }), - buildTextField({ - id: 'representative.email', - title: representativeInfo.labels.email, - backgroundColor: 'blue', - variant: 'email', - width: 'half', - required: true, - maxLength: 100, - condition: (formValue) => - !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), - }), - buildTextField({ - id: 'representative.phoneNumber', - title: representativeInfo.labels.tel, - backgroundColor: 'blue', - format: '###-####', - variant: 'tel', - width: 'half', - condition: (formValue) => - !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), - }), - buildCustomField( - { - id: 'representativeInfo.custom', - title: '', - doesNotRequireAnswer: true, - component: 'HiddenInformation', - }, - { - id: 'representativeInfo', - }, - ), - ], - }), - ], - }), - // School information if school accident - buildSubSection({ - id: 'schoolInfo.subSection', - title: schoolInfo.general.title, - condition: (formValue) => - isStudiesAccident(formValue) && - !isInternshipStudiesAccident(formValue) && - !isReportingOnBehalfOfEmployee(formValue), - children: [ - buildMultiField({ - id: 'schoolInfo', - title: schoolInfo.general.title, - description: schoolInfo.general.description, - children: [ - buildTextField({ - id: 'companyInfo.name', - title: schoolInfo.labels.name, - backgroundColor: 'blue', - required: true, - width: 'half', - maxLength: 100, - }), - buildTextField({ - id: 'companyInfo.nationalRegistrationId', - title: schoolInfo.labels.nationalId, - backgroundColor: 'blue', - format: '######-####', - required: true, - width: 'half', - }), - // buildCheckboxField({ - // id: 'isRepresentativeOfCompanyOrInstitue', - // title: '', - // defaultValue: [], - // large: false, - // backgroundColor: 'white', - // options: [ - // { - // value: YES, - // label: schoolInfo.labels.checkBox, - // }, - // ], - // }), - buildDescriptionField({ - id: 'schoolInfo.descriptionField', - description: schoolInfo.labels.subDescription, - space: 'containerGutter', - titleVariant: 'h5', - title: schoolInfo.labels.descriptionField, - condition: (formValue) => - !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), - }), - // These should all be required if the user is not the representative of the company. - // Should look into if we can require conditionally - buildTextField({ - id: 'representative.name', - title: representativeInfo.labels.name, - backgroundColor: 'blue', - required: true, - width: 'half', - maxLength: 100, - condition: (formValue) => - !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), - }), - buildTextField({ - id: 'representative.nationalId', - title: representativeInfo.labels.nationalId, - backgroundColor: 'blue', - required: true, - width: 'half', - format: '######-####', - condition: (formValue) => - !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), - }), - buildTextField({ - id: 'representative.email', - title: representativeInfo.labels.email, - backgroundColor: 'blue', - variant: 'email', - width: 'half', - maxLength: 100, - required: true, - condition: (formValue) => - !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), - }), - buildTextField({ - id: 'representative.phoneNumber', - title: representativeInfo.labels.tel, - backgroundColor: 'blue', - format: '###-####', - variant: 'tel', - width: 'half', - condition: (formValue) => - !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), - }), - buildCustomField( - { - id: 'schoolInfo.custom', - title: '', - doesNotRequireAnswer: true, - component: 'HiddenInformation', - }, - { - id: 'representativeInfo', - }, - ), - ], - }), - ], - }), - // fishery information if fisherman - buildSubSection({ - id: 'fishingCompanyInfo.subSection', - title: (application) => - isReportingOnBehalfOfEmployee(application.answers) - ? fishingCompanyInfo.general.informationAboutShipTitle - : fishingCompanyInfo.general.title, - condition: (formValue) => isFishermanAccident(formValue), - children: [ - buildMultiField({ - id: 'fishingShipInfo', - title: fishingCompanyInfo.general.informationAboutShipTitle, - description: - fishingCompanyInfo.general.informationAboutShipDescription, - children: [ - buildTextField({ - id: 'fishingShipInfo.shipName', - title: fishingCompanyInfo.labels.shipName, - backgroundColor: 'blue', - width: 'half', - required: true, - maxLength: 100, - }), - buildTextField({ - id: 'fishingShipInfo.shipCharacters', - title: fishingCompanyInfo.labels.shipCharacters, - backgroundColor: 'blue', - width: 'half', - required: true, - maxLength: 100, - }), - buildTextField({ - id: 'fishingShipInfo.homePort', - title: fishingCompanyInfo.labels.homePort, - backgroundColor: 'blue', - width: 'half', - maxLength: 100, - }), - buildTextField({ - id: 'fishingShipInfo.shipRegisterNumber', - title: fishingCompanyInfo.labels.shipRegisterNumber, - backgroundColor: 'blue', - width: 'half', - maxLength: 100, - }), - ], - }), - buildMultiField({ - id: 'fishingCompanyInfo', - title: fishingCompanyInfo.general.title, - description: fishingCompanyInfo.general.description, - condition: (formValue) => !isReportingOnBehalfOfEmployee(formValue), - children: [ - buildTextField({ - id: 'companyInfo.name', - title: fishingCompanyInfo.labels.name, - backgroundColor: 'blue', - required: true, - width: 'half', - maxLength: 100, - }), - buildTextField({ - id: 'companyInfo.nationalRegistrationId', - title: fishingCompanyInfo.labels.nationalId, - backgroundColor: 'blue', - format: '######-####', - required: true, - width: 'half', - }), - // buildCheckboxField({ - // id: 'isRepresentativeOfCompanyOrInstitue', - // title: '', - // defaultValue: [], - // large: false, - // backgroundColor: 'white', - // options: [ - // { - // value: YES, - // label: fishingCompanyInfo.labels.checkBox, - // }, - // ], - // }), - buildDescriptionField({ - id: 'fishingCompanyInfo.descriptionField', - description: fishingCompanyInfo.labels.subDescription, - space: 'containerGutter', - titleVariant: 'h5', - title: fishingCompanyInfo.labels.descriptionField, - condition: (formValue) => - !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), - }), - // These should all be required if the user is not the representative of the company. - // Should look into if we can require conditionally - buildTextField({ - id: 'representative.name', - title: representativeInfo.labels.name, - backgroundColor: 'blue', - required: true, - width: 'half', - maxLength: 100, - condition: (formValue) => - !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), - }), - buildTextField({ - id: 'representative.nationalId', - title: representativeInfo.labels.nationalId, - backgroundColor: 'blue', - required: true, - width: 'half', - format: '######-####', - condition: (formValue) => - !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), - }), - buildTextField({ - id: 'representative.email', - title: representativeInfo.labels.email, - backgroundColor: 'blue', - variant: 'email', - width: 'half', - maxLength: 100, - required: true, - condition: (formValue) => - !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), - }), - buildTextField({ - id: 'representative.phoneNumber', - title: representativeInfo.labels.tel, - backgroundColor: 'blue', - format: '###-####', - variant: 'tel', - width: 'half', - condition: (formValue) => - !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), - }), - buildCustomField( - { - id: 'representativeInfo.custom', - title: '', - doesNotRequireAnswer: true, - component: 'HiddenInformation', - }, - { - id: 'representativeInfo', - }, - ), - ], - }), - ], - }), - // Sports club information when the injured has a sports related accident - buildSubSection({ - id: 'sportsClubInfo.subSection', - title: sportsClubInfo.general.title, - condition: (formValue) => - isProfessionalAthleteAccident(formValue) && - !isReportingOnBehalfOfEmployee(formValue), - children: [ - buildMultiField({ - id: 'sportsClubInfo', - title: sportsClubInfo.general.title, - description: sportsClubInfo.general.description, - children: [ - buildTextField({ - id: 'companyInfo.name', - title: sportsClubInfo.labels.name, - backgroundColor: 'blue', - width: 'half', - required: true, - maxLength: 100, - }), - buildTextField({ - id: 'companyInfo.nationalRegistrationId', - title: sportsClubInfo.labels.nationalId, - backgroundColor: 'blue', - format: '######-####', - required: true, - width: 'half', - }), - // buildCheckboxField({ - // id: 'isRepresentativeOfCompanyOrInstitue', - // title: '', - // defaultValue: [], - // large: false, - // backgroundColor: 'white', - // options: [ - // { - // value: YES, - // label: sportsClubInfo.labels.checkBox, - // }, - // ], - // }), - buildDescriptionField({ - id: 'sportsClubInfo.descriptionField', - description: sportsClubInfo.labels.subDescription, - space: 'containerGutter', - titleVariant: 'h5', - title: sportsClubInfo.labels.descriptionField, - condition: (formValue) => - !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), - }), - // These should all be required if the user is not the representative of the company. - // Should look into if we can require conditionally - buildTextField({ - id: 'representative.name', - title: representativeInfo.labels.name, - backgroundColor: 'blue', - required: true, - width: 'half', - maxLength: 100, - condition: (formValue) => - !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), - }), - buildTextField({ - id: 'representative.nationalId', - title: representativeInfo.labels.nationalId, - backgroundColor: 'blue', - required: true, - width: 'half', - format: '######-####', - condition: (formValue) => - !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), - }), - buildTextField({ - id: 'representative.email', - title: representativeInfo.labels.email, - backgroundColor: 'blue', - variant: 'email', - width: 'half', - maxLength: 100, - required: true, - condition: (formValue) => - !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), - }), - buildTextField({ - id: 'representative.phoneNumber', - title: representativeInfo.labels.tel, - backgroundColor: 'blue', - format: '###-####', - variant: 'tel', - width: 'half', - condition: (formValue) => - !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), - }), - buildCustomField( - { - id: 'representativeInfo.custom', - title: '', - doesNotRequireAnswer: true, - component: 'HiddenInformation', - }, - { - id: 'representativeInfo', - }, - ), - ], - }), - ], - }), - // Rescue squad information when accident is related to rescue squad - buildSubSection({ - id: 'rescueSquadInfo.subSection', - title: rescueSquadInfo.general.title, - condition: (formValue) => - isRescueWorkAccident(formValue) && - !isReportingOnBehalfOfEmployee(formValue), - children: [ - buildMultiField({ - id: 'rescueSquad', - title: rescueSquadInfo.general.title, - description: rescueSquadInfo.general.description, - children: [ - buildTextField({ - id: 'companyInfo.name', - title: rescueSquadInfo.labels.name, - backgroundColor: 'blue', - width: 'half', - required: true, - maxLength: 100, - }), - buildTextField({ - id: 'companyInfo.nationalRegistrationId', - title: rescueSquadInfo.labels.nationalId, - backgroundColor: 'blue', - format: '######-####', - required: true, - width: 'half', - }), - // buildCheckboxField({ - // id: 'isRepresentativeOfCompanyOrInstitue', - // title: '', - // defaultValue: [], - // large: false, - // backgroundColor: 'white', - // options: [ - // { - // value: YES, - // label: rescueSquadInfo.labels.checkBox, - // }, - // ], - // }), - buildDescriptionField({ - id: 'rescueSquadInfo.descriptionField', - description: rescueSquadInfo.labels.subDescription, - space: 'containerGutter', - titleVariant: 'h5', - title: rescueSquadInfo.labels.descriptionField, - condition: (formValue) => - !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), - }), - buildTextField({ - id: 'representative.name', - title: representativeInfo.labels.name, - backgroundColor: 'blue', - required: true, - width: 'half', - maxLength: 100, - condition: (formValue) => - !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), - }), - buildTextField({ - id: 'representative.nationalId', - title: representativeInfo.labels.nationalId, - backgroundColor: 'blue', - required: true, - width: 'half', - format: '######-####', - condition: (formValue) => - !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), - }), - buildTextField({ - id: 'representative.email', - title: representativeInfo.labels.email, - backgroundColor: 'blue', - variant: 'email', - width: 'half', - maxLength: 100, - required: true, - condition: (formValue) => - !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), - }), - buildTextField({ - id: 'representative.phoneNumber', - title: representativeInfo.labels.tel, - backgroundColor: 'blue', - format: '###-####', - variant: 'tel', - width: 'half', - doesNotRequireAnswer: true, - condition: (formValue) => - !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), - }), - buildCustomField( - { - id: 'representativeInfo.custom', - title: '', - doesNotRequireAnswer: true, - component: 'HiddenInformation', - }, - { - id: 'representativeInfo', - }, - ), - ], - }), - ], - }), - ], -}) diff --git a/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/aboutTheAccidentSection/accidentDetailSubSection.ts b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/aboutTheAccidentSection/accidentDetailSubSection.ts new file mode 100644 index 000000000000..dfe52a39a6d8 --- /dev/null +++ b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/aboutTheAccidentSection/accidentDetailSubSection.ts @@ -0,0 +1,100 @@ +import { + buildAlertMessageField, + buildCustomField, + buildDescriptionField, + buildMultiField, + buildSubSection, + buildTextField, +} from '@island.is/application/core' +import { accidentDetails } from '../../../lib/messages' +import { isDateOlderThanAYear } from '../../../utils' +import { isHealthInsured } from '../../../utils/isHealthInsured' + +// Details of the accident +export const accidentDetailsSubSection = buildSubSection({ + id: 'accidentDetails.section', + title: accidentDetails.general.sectionTitle, + children: [ + buildMultiField({ + id: 'accidentDetails', + title: accidentDetails.general.sectionTitle, + description: accidentDetails.general.description, + children: [ + buildCustomField({ + id: 'accidentDetails.dateOfAccident', + title: accidentDetails.labels.date, + component: 'DateOfAccident', + width: 'half', + }), + buildTextField({ + id: 'accidentDetails.timeOfAccident', + title: accidentDetails.labels.time, + placeholder: accidentDetails.placeholder.time, + backgroundColor: 'blue', + required: true, + width: 'half', + format: '##:##', + }), + buildAlertMessageField({ + id: 'accidentDetails.moreThanAYearAlertMessage', + title: accidentDetails.general.moreThanAYearAlertTitle, + message: accidentDetails.general.moreThanAYearAlertMessage, + width: 'full', + alertType: 'warning', + condition: (formValue) => isDateOlderThanAYear(formValue), + marginBottom: 0, + }), + buildAlertMessageField({ + id: 'accidentDetails.notHealthInsuredAlertMessage', + title: accidentDetails.general.insuranceAlertTitle, + message: accidentDetails.general.insuranceAlertText, + width: 'full', + alertType: 'warning', + condition: (formValue) => !isHealthInsured(formValue), + marginBottom: 0, + }), + buildTextField({ + id: 'accidentDetails.descriptionOfAccident', + title: accidentDetails.labels.description, + placeholder: accidentDetails.placeholder.description, + backgroundColor: 'blue', + required: true, + rows: 10, + variant: 'textarea', + maxLength: 2000, + }), + buildTextField({ + id: 'accidentDetails.accidentSymptoms', + title: accidentDetails.labels.symptoms, + placeholder: accidentDetails.placeholder.symptoms, + backgroundColor: 'blue', + required: true, + rows: 10, + variant: 'textarea', + maxLength: 2000, + }), + buildDescriptionField({ + id: 'accidentDetails.descriptionField', + space: 'containerGutter', + titleVariant: 'h5', + title: accidentDetails.labels.doctorVisit, + width: 'full', + }), + buildCustomField({ + id: 'accidentDetails.dateOfDoctorVisit', + title: accidentDetails.labels.date, + component: 'DateOfAccident', + width: 'half', + }), + buildTextField({ + id: 'accidentDetails.timeOfDoctorVisit', + title: accidentDetails.labels.time, + placeholder: accidentDetails.placeholder.doctorVisitTime, + backgroundColor: 'blue', + width: 'half', + format: '##:##', + }), + ], + }), + ], +}) diff --git a/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/aboutTheAccidentSection/accidentTypeSubSection.ts b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/aboutTheAccidentSection/accidentTypeSubSection.ts new file mode 100644 index 000000000000..387eb21ad6c9 --- /dev/null +++ b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/aboutTheAccidentSection/accidentTypeSubSection.ts @@ -0,0 +1,17 @@ +import { buildRadioField, buildSubSection } from '@island.is/application/core' +import { accidentType } from '../../../lib/messages' +import { getAccidentTypeOptions } from '../../../utils' + +export const accidentTypeSubSection = buildSubSection({ + id: 'accidentType.section', + title: accidentType.general.subsectionTitle, + children: [ + buildRadioField({ + id: 'accidentType.radioButton', + width: 'half', + title: accidentType.general.heading, + description: accidentType.general.description, + options: (formValue) => getAccidentTypeOptions(formValue.answers), + }), + ], +}) diff --git a/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/aboutTheAccidentSection/attachmentsSubSection.ts b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/aboutTheAccidentSection/attachmentsSubSection.ts new file mode 100644 index 000000000000..8d666ce9d97c --- /dev/null +++ b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/aboutTheAccidentSection/attachmentsSubSection.ts @@ -0,0 +1,230 @@ +import { + buildAlertMessageField, + buildFileUploadField, + buildMultiField, + buildRadioField, + buildSubSection, + getValueViaPath, +} from '@island.is/application/core' +import { + addDocuments, + application, + attachments, + error, + fatalAccident, + fatalAccidentAttachment, + injuredPersonInformation, +} from '../../../lib/messages' +import { + isFatalAccident, + isReportingOnBehalfOfInjured, + isRepresentativeOfCompanyOrInstitute, +} from '../../../utils' +import { AttachmentsEnum } from '../../../types' +import { FILE_SIZE_LIMIT, NO, UPLOAD_ACCEPT, YES } from '../../../constants' + +// Injury certificate and fatal accident section +export const attachmentsSubSection = buildSubSection({ + id: 'attachments.section', + title: attachments.general.sectionTitle, + children: [ + buildMultiField({ + id: 'attachments', + title: attachments.general.heading, + children: [ + buildRadioField({ + id: 'injuryCertificate.answer', + title: '', + description: attachments.general.description, + required: true, + options: (application) => + isRepresentativeOfCompanyOrInstitute(application.answers) + ? [ + { + value: AttachmentsEnum.INJURYCERTIFICATE, + label: attachments.labels.injuryCertificate, + }, + { + value: AttachmentsEnum.SENDCERTIFICATELATER, + label: attachments.labels.sendCertificateLater, + }, + ] + : [ + { + value: AttachmentsEnum.INJURYCERTIFICATE, + label: attachments.labels.injuryCertificate, + }, + { + value: AttachmentsEnum.HOSPITALSENDSCERTIFICATE, + label: attachments.labels.hospitalSendsCertificate, + }, + { + value: AttachmentsEnum.SENDCERTIFICATELATER, + label: attachments.labels.sendCertificateLater, + }, + ], + }), + buildAlertMessageField({ + id: 'attachments.injuryCertificate.alert', + title: attachments.labels.alertMessage, + message: attachments.general.alertMessage, + doesNotRequireAnswer: true, + condition: (formValue) => + getValueViaPath(formValue, 'injuryCertificate.answer') === + AttachmentsEnum.SENDCERTIFICATELATER, + alertType: 'warning', + }), + ], + }), + buildMultiField({ + id: 'attachments.injuryCertificateFile.subSection', + title: attachments.general.heading, + children: [ + buildFileUploadField({ + id: 'attachments.injuryCertificateFile.file', + title: attachments.general.heading, + maxSize: FILE_SIZE_LIMIT, + maxSizeErrorText: error.attachmentMaxSizeError, + uploadAccept: UPLOAD_ACCEPT, + uploadHeader: injuredPersonInformation.upload.uploadHeader, + uploadDescription: attachments.general.uploadDescription, + uploadButtonLabel: attachments.general.uploadButtonLabel, + introduction: attachments.general.uploadIntroduction, + }), + ], + condition: (formValue) => + getValueViaPath(formValue, 'injuryCertificate.answer') === + AttachmentsEnum.INJURYCERTIFICATE, + }), + buildMultiField({ + id: 'fatalAccidentMulti.section', + title: fatalAccident.general.sectionTitle, + condition: (formValue) => isReportingOnBehalfOfInjured(formValue), + children: [ + buildRadioField({ + id: 'wasTheAccidentFatal', + title: '', + backgroundColor: 'blue', + width: 'half', + required: true, + options: [ + { value: YES, label: application.general.yesOptionLabel }, + { value: NO, label: application.general.noOptionLabel }, + ], + }), + ], + }), + buildMultiField({ + id: 'fatalAccidentUploadDeathCertificateNowMulti', + title: fatalAccidentAttachment.labels.title, + description: fatalAccidentAttachment.labels.description, + condition: (formValue) => + isReportingOnBehalfOfInjured(formValue) && + formValue.wasTheAccidentFatal === YES, + children: [ + buildRadioField({ + id: 'fatalAccidentUploadDeathCertificateNow', + title: '', + backgroundColor: 'blue', + required: true, + options: [ + { + value: YES, + label: fatalAccidentAttachment.options.addAttachmentsNow, + }, + { + value: NO, + label: fatalAccidentAttachment.options.addAttachmentsLater, + }, + ], + }), + buildAlertMessageField({ + id: 'attachments.injuryCertificate.alert', + title: fatalAccident.alertMessage.title, + message: fatalAccident.alertMessage.description, + doesNotRequireAnswer: true, + alertType: 'warning', + condition: (formValue) => + getValueViaPath( + formValue, + 'fatalAccidentUploadDeathCertificateNow', + ) === NO, + }), + ], + }), + + buildMultiField({ + id: 'attachments.deathCertificateFile.subSection', + title: attachments.general.uploadTitle, + condition: (formValue) => + isReportingOnBehalfOfInjured(formValue) && + isFatalAccident(formValue) && + formValue.fatalAccidentUploadDeathCertificateNow === YES, + children: [ + buildFileUploadField({ + id: 'attachments.deathCertificateFile.file', + title: attachments.general.uploadHeader, + maxSize: FILE_SIZE_LIMIT, + maxSizeErrorText: error.attachmentMaxSizeError, + uploadAccept: UPLOAD_ACCEPT, + uploadHeader: attachments.general.uploadHeader, + uploadDescription: attachments.general.uploadDescription, + uploadButtonLabel: attachments.general.uploadButtonLabel, + introduction: attachments.general.uploadIntroduction, + }), + ], + }), + buildMultiField({ + id: 'attachments.additionalFilesMulti', + title: attachments.general.heading, + children: [ + buildRadioField({ + id: 'additionalAttachments.answer', + title: '', + description: attachments.general.additionalAttachmentDescription, + required: true, + options: () => [ + { + value: AttachmentsEnum.ADDITIONALNOW, + label: attachments.labels.additionalNow, + }, + { + value: AttachmentsEnum.ADDITIONALLATER, + label: attachments.labels.additionalLater, + }, + ], + }), + buildAlertMessageField({ + id: 'attachments.injuryCertificate.alert', + title: attachments.labels.alertMessage, + message: attachments.general.alertMessage, + alertType: 'warning', + doesNotRequireAnswer: true, + condition: (formValue) => + getValueViaPath(formValue, 'additionalAttachments.answer') === + AttachmentsEnum.ADDITIONALLATER, + }), + ], + }), + buildMultiField({ + id: 'attachments.additionalAttachments.subSection', + title: attachments.general.heading, + condition: (formValue) => + getValueViaPath(formValue, 'additionalAttachments.answer') === + AttachmentsEnum.ADDITIONALNOW, + children: [ + buildFileUploadField({ + id: 'attachments.additionalFiles.file', + title: attachments.general.heading, + maxSize: FILE_SIZE_LIMIT, + maxSizeErrorText: error.attachmentMaxSizeError, + uploadAccept: UPLOAD_ACCEPT, + uploadHeader: addDocuments.general.uploadHeader, + uploadDescription: addDocuments.general.uploadDescription, + uploadButtonLabel: addDocuments.general.uploadButtonLabel, + introduction: addDocuments.general.additionalDocumentsDescription, + }), + ], + }), + ], +}) diff --git a/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/aboutTheAccidentSection/companyInfoSubSection.ts b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/aboutTheAccidentSection/companyInfoSubSection.ts new file mode 100644 index 000000000000..05cc9f837316 --- /dev/null +++ b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/aboutTheAccidentSection/companyInfoSubSection.ts @@ -0,0 +1,117 @@ +import { + buildCustomField, + buildDescriptionField, + buildMultiField, + buildSubSection, + buildTextField, +} from '@island.is/application/core' +import { companyInfo, representativeInfo } from '../../../lib/messages' +import { + isAgricultureAccident, + isGeneralWorkplaceAccident, + isHomeActivitiesAccident, + isInjuredAndRepresentativeOfCompanyOrInstitute, + isInternshipStudiesAccident, + isReportingOnBehalfOfEmployee, +} from '../../../utils' + +// Company information if work accident without the injured being a fisherman or in agriculture +export const companyInfoSubSection = buildSubSection({ + id: 'companyInfo.subSection', + title: companyInfo.general.title, + condition: (formValue) => + !isAgricultureAccident(formValue) && + !isReportingOnBehalfOfEmployee(formValue) && + !isHomeActivitiesAccident(formValue) && + (isGeneralWorkplaceAccident(formValue) || + isInternshipStudiesAccident(formValue)), + children: [ + buildMultiField({ + id: 'companyInfo', + title: companyInfo.general.title, + description: companyInfo.general.description, + children: [ + buildTextField({ + id: 'companyInfo.name', + title: companyInfo.labels.name, + backgroundColor: 'blue', + required: true, + width: 'half', + maxLength: 100, + }), + buildTextField({ + id: 'companyInfo.nationalRegistrationId', + title: companyInfo.labels.nationalId, + backgroundColor: 'blue', + width: 'half', + format: '######-####', + required: true, + }), + buildDescriptionField({ + id: 'companyInfo.descriptionField', + description: companyInfo.labels.subDescription, + space: 'containerGutter', + titleVariant: 'h5', + title: companyInfo.labels.descriptionField, + condition: (formValue) => + !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), + }), + + // These should all be required if the user is not the representative of the company. + // Should look into if we can require conditionally + buildTextField({ + id: 'representative.name', + title: representativeInfo.labels.name, + backgroundColor: 'blue', + required: true, + width: 'half', + maxLength: 100, + condition: (formValue) => + !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), + }), + buildTextField({ + id: 'representative.nationalId', + title: representativeInfo.labels.nationalId, + backgroundColor: 'blue', + required: true, + width: 'half', + format: '######-####', + condition: (formValue) => + !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), + }), + buildTextField({ + id: 'representative.email', + title: representativeInfo.labels.email, + backgroundColor: 'blue', + variant: 'email', + width: 'half', + required: true, + maxLength: 100, + condition: (formValue) => + !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), + }), + buildTextField({ + id: 'representative.phoneNumber', + title: representativeInfo.labels.tel, + backgroundColor: 'blue', + format: '###-####', + variant: 'tel', + width: 'half', + condition: (formValue) => + !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), + }), + buildCustomField( + { + id: 'representativeInfo.custom', + title: '', + doesNotRequireAnswer: true, + component: 'HiddenInformation', + }, + { + id: 'representativeInfo', + }, + ), + ], + }), + ], +}) diff --git a/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/aboutTheAccidentSection/fishingCompanyInfoSubSection.ts b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/aboutTheAccidentSection/fishingCompanyInfoSubSection.ts new file mode 100644 index 000000000000..03efd89ffdaf --- /dev/null +++ b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/aboutTheAccidentSection/fishingCompanyInfoSubSection.ts @@ -0,0 +1,149 @@ +import { + buildCustomField, + buildDescriptionField, + buildMultiField, + buildSubSection, + buildTextField, +} from '@island.is/application/core' +import { + isFishermanAccident, + isInjuredAndRepresentativeOfCompanyOrInstitute, + isReportingOnBehalfOfEmployee, +} from '../../../utils' +import { fishingCompanyInfo, representativeInfo } from '../../../lib/messages' + +// fishery information if fisherman +export const fishingCompanyInfoSubSection = buildSubSection({ + id: 'fishingCompanyInfo.subSection', + title: (application) => + isReportingOnBehalfOfEmployee(application.answers) + ? fishingCompanyInfo.general.informationAboutShipTitle + : fishingCompanyInfo.general.title, + condition: (formValue) => isFishermanAccident(formValue), + children: [ + buildMultiField({ + id: 'fishingShipInfo', + title: fishingCompanyInfo.general.informationAboutShipTitle, + description: fishingCompanyInfo.general.informationAboutShipDescription, + children: [ + buildTextField({ + id: 'fishingShipInfo.shipName', + title: fishingCompanyInfo.labels.shipName, + backgroundColor: 'blue', + width: 'half', + required: true, + maxLength: 100, + }), + buildTextField({ + id: 'fishingShipInfo.shipCharacters', + title: fishingCompanyInfo.labels.shipCharacters, + backgroundColor: 'blue', + width: 'half', + required: true, + maxLength: 100, + }), + buildTextField({ + id: 'fishingShipInfo.homePort', + title: fishingCompanyInfo.labels.homePort, + backgroundColor: 'blue', + width: 'half', + maxLength: 100, + }), + buildTextField({ + id: 'fishingShipInfo.shipRegisterNumber', + title: fishingCompanyInfo.labels.shipRegisterNumber, + backgroundColor: 'blue', + width: 'half', + maxLength: 100, + }), + ], + }), + buildMultiField({ + id: 'fishingCompanyInfo', + title: fishingCompanyInfo.general.title, + description: fishingCompanyInfo.general.description, + condition: (formValue) => !isReportingOnBehalfOfEmployee(formValue), + children: [ + buildTextField({ + id: 'companyInfo.name', + title: fishingCompanyInfo.labels.name, + backgroundColor: 'blue', + required: true, + width: 'half', + maxLength: 100, + }), + buildTextField({ + id: 'companyInfo.nationalRegistrationId', + title: fishingCompanyInfo.labels.nationalId, + backgroundColor: 'blue', + format: '######-####', + required: true, + width: 'half', + }), + buildDescriptionField({ + id: 'fishingCompanyInfo.descriptionField', + description: fishingCompanyInfo.labels.subDescription, + space: 'containerGutter', + titleVariant: 'h5', + title: fishingCompanyInfo.labels.descriptionField, + condition: (formValue) => + !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), + }), + // These should all be required if the user is not the representative of the company. + // Should look into if we can require conditionally + buildTextField({ + id: 'representative.name', + title: representativeInfo.labels.name, + backgroundColor: 'blue', + required: true, + width: 'half', + maxLength: 100, + condition: (formValue) => + !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), + }), + buildTextField({ + id: 'representative.nationalId', + title: representativeInfo.labels.nationalId, + backgroundColor: 'blue', + required: true, + width: 'half', + format: '######-####', + condition: (formValue) => + !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), + }), + buildTextField({ + id: 'representative.email', + title: representativeInfo.labels.email, + backgroundColor: 'blue', + variant: 'email', + width: 'half', + maxLength: 100, + required: true, + condition: (formValue) => + !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), + }), + buildTextField({ + id: 'representative.phoneNumber', + title: representativeInfo.labels.tel, + backgroundColor: 'blue', + format: '###-####', + variant: 'tel', + width: 'half', + condition: (formValue) => + !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), + }), + buildCustomField( + { + id: 'representativeInfo.custom', + title: '', + doesNotRequireAnswer: true, + component: 'HiddenInformation', + }, + { + id: 'representativeInfo', + }, + ), + ], + }), + ], +}) diff --git a/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/aboutTheAccidentSection/index.ts b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/aboutTheAccidentSection/index.ts new file mode 100644 index 000000000000..8c15c70a4bd3 --- /dev/null +++ b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/aboutTheAccidentSection/index.ts @@ -0,0 +1,33 @@ +import { buildSection } from '@island.is/application/core' +import { accidentType } from '../../../lib/messages' +import { accidentTypeSubSection } from './accidentTypeSubSection' +import { workAccidentSubSection } from './workAccidentSubSection' +import { studiesAccidentSubSection } from './studiesAccidentSubSection' +import { locationSubSection } from './locationSubSection' +import { workMachineSubSection } from './workMachineSubSection' +import { accidentDetailsSubSection } from './accidentDetailSubSection' +import { attachmentsSubSection } from './attachmentsSubSection' +import { schoolInfoSubSection } from './schoolInfoSubSection' +import { fishingCompanyInfoSubSection } from './fishingCompanyInfoSubSection' +import { sportsClubInfoSubSection } from './sportsClubInfoSubSection' +import { rescueSquadInfoSubSection } from './rescueSquadInfoSubSection' +import { companyInfoSubSection } from './companyInfoSubSection' + +export const aboutTheAccidentSection = buildSection({ + id: 'accidentType.section', + title: accidentType.general.sectionTitle, + children: [ + accidentTypeSubSection, + workAccidentSubSection, + studiesAccidentSubSection, + locationSubSection, + workMachineSubSection, + accidentDetailsSubSection, + attachmentsSubSection, + companyInfoSubSection, + schoolInfoSubSection, + fishingCompanyInfoSubSection, + sportsClubInfoSubSection, + rescueSquadInfoSubSection, + ], +}) diff --git a/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/aboutTheAccidentSection/locationSubSection.ts b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/aboutTheAccidentSection/locationSubSection.ts new file mode 100644 index 000000000000..86c5459e1169 --- /dev/null +++ b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/aboutTheAccidentSection/locationSubSection.ts @@ -0,0 +1,336 @@ +import { + buildAlertMessageField, + buildMultiField, + buildRadioField, + buildSubSection, + buildTextField, +} from '@island.is/application/core' +import { + accidentLocation, + application, + locationAndPurpose, + sportsClubInfo, +} from '../../../lib/messages' +import { + hideLocationAndPurpose, + isAgricultureAccident, + isFishermanAccident, + isGeneralWorkplaceAccident, + isHomeActivitiesAccident, + isInternshipStudiesAccident, + isProfessionalAthleteAccident, + isRescueWorkAccident, + isStudiesAccident, +} from '../../../utils' +import { NO, YES } from '../../../constants' +import { isSportAccidentAndEmployee } from '../../../utils/isSportAccidentAndEmployee' +import { + AgricultureAccidentLocationEnum, + FishermanWorkplaceAccidentLocationEnum, + FishermanWorkplaceAccidentShipLocationEnum, + GeneralWorkplaceAccidentLocationEnum, + ProfessionalAthleteAccidentLocationEnum, + RescueWorkAccidentLocationEnum, + StudiesAccidentLocationEnum, +} from '../../../types' + +// Location Subsection +export const locationSubSection = buildSubSection({ + id: 'location.subSection', + title: 'Staðsetning', + children: [ + buildMultiField({ + id: 'sportsClubInfo.employee.field', + title: sportsClubInfo.employee.title, + condition: (formValue) => isProfessionalAthleteAccident(formValue), + children: [ + buildRadioField({ + id: 'onPayRoll.answer', + width: 'half', + title: '', + options: [ + { + value: YES, + label: application.general.yesOptionLabel, + }, + { + value: NO, + label: application.general.noOptionLabel, + }, + ], + }), + buildAlertMessageField({ + id: 'attachments.injuryCertificate.alert', + title: application.labels.warningTitle, + message: application.labels.warningMessage, + alertType: 'info', + doesNotRequireAnswer: true, + condition: (formValue) => isSportAccidentAndEmployee(formValue), + }), + ], + }), + + // Accident location section + // location of home related accident + buildMultiField({ + id: 'accidentLocation.homeAccident', + title: accidentLocation.homeAccidentLocation.title, + description: accidentLocation.homeAccidentLocation.description, + condition: (formValue) => isHomeActivitiesAccident(formValue), + children: [ + buildTextField({ + id: 'homeAccident.address', + title: accidentLocation.homeAccidentLocation.address, + backgroundColor: 'blue', + required: true, + maxLength: 100, + }), + buildTextField({ + id: 'homeAccident.postalCode', + title: accidentLocation.homeAccidentLocation.postalCode, + backgroundColor: 'blue', + width: 'half', + format: '###', + required: true, + }), + buildTextField({ + id: 'homeAccident.community', + title: accidentLocation.homeAccidentLocation.community, + backgroundColor: 'blue', + width: 'half', + required: true, + maxLength: 100, + }), + buildTextField({ + id: 'homeAccident.moreDetails', + title: accidentLocation.homeAccidentLocation.moreDetails, + placeholder: + accidentLocation.homeAccidentLocation.moreDetailsPlaceholder, + backgroundColor: 'blue', + rows: 4, + variant: 'textarea', + maxLength: 2000, + }), + ], + }), + // location of general work related accident + buildMultiField({ + id: 'accidentLocation.generalWorkAccident', + title: accidentLocation.general.heading, + description: accidentLocation.general.description, + condition: (formValue) => + isGeneralWorkplaceAccident(formValue) || + isSportAccidentAndEmployee(formValue), + children: [ + buildRadioField({ + id: 'accidentLocation.answer', + title: '', + options: [ + { + value: GeneralWorkplaceAccidentLocationEnum.ATTHEWORKPLACE, + label: accidentLocation.generalWorkAccident.atTheWorkplace, + }, + { + value: GeneralWorkplaceAccidentLocationEnum.TOORFROMTHEWORKPLACE, + label: accidentLocation.generalWorkAccident.toOrFromTheWorkplace, + }, + { + value: GeneralWorkplaceAccidentLocationEnum.OTHER, + label: accidentLocation.generalWorkAccident.other, + }, + ], + }), + ], + }), + // location of rescue work related accident + buildMultiField({ + id: 'accidentLocation.rescueWorkAccident', + title: accidentLocation.general.heading, + description: accidentLocation.rescueWorkAccident.description, + condition: (formValue) => { + return isRescueWorkAccident(formValue) + }, + children: [ + buildRadioField({ + id: 'accidentLocation.answer', + title: '', + options: [ + { + value: RescueWorkAccidentLocationEnum.DURINGRESCUE, + label: accidentLocation.rescueWorkAccident.duringRescue, + }, + { + value: RescueWorkAccidentLocationEnum.TOORFROMRESCUE, + label: accidentLocation.rescueWorkAccident.toOrFromRescue, + }, + { + value: RescueWorkAccidentLocationEnum.OTHER, + label: accidentLocation.rescueWorkAccident.other, + }, + ], + }), + ], + }), + // location of studies related accident + buildMultiField({ + id: 'accidentLocation.studiesAccident', + title: accidentLocation.studiesAccidentLocation.heading, + description: accidentLocation.studiesAccidentLocation.description, + condition: (formValue) => + isStudiesAccident(formValue) && !isInternshipStudiesAccident(formValue), + children: [ + buildRadioField({ + id: 'accidentLocation.answer', + title: '', + options: [ + { + value: StudiesAccidentLocationEnum.ATTHESCHOOL, + label: accidentLocation.studiesAccidentLocation.atTheSchool, + }, + { + value: StudiesAccidentLocationEnum.OTHER, + label: accidentLocation.studiesAccidentLocation.other, + }, + ], + }), + ], + }), + // location of fisherman related accident + buildMultiField({ + id: 'accidentLocation.fishermanAccident', + title: accidentLocation.general.heading, + description: accidentLocation.general.description, + condition: (formValue) => isFishermanAccident(formValue), + children: [ + buildRadioField({ + id: 'accidentLocation.answer', + title: '', + options: [ + { + value: FishermanWorkplaceAccidentLocationEnum.ONTHESHIP, + label: accidentLocation.fishermanAccident.onTheShip, + }, + { + value: + FishermanWorkplaceAccidentLocationEnum.TOORFROMTHEWORKPLACE, + label: accidentLocation.fishermanAccident.toOrFromTheWorkplace, + }, + { + value: FishermanWorkplaceAccidentLocationEnum.OTHER, + label: accidentLocation.fishermanAccident.other, + }, + ], + }), + ], + }), + // location of sports related accident + buildMultiField({ + id: 'accidentLocation.professionalAthleteAccident', + title: accidentLocation.general.heading, + description: accidentLocation.general.description, + condition: (formValue) => + isProfessionalAthleteAccident(formValue) && + !isSportAccidentAndEmployee(formValue), + children: [ + buildRadioField({ + id: 'accidentLocation.answer', + title: '', + options: [ + { + value: + ProfessionalAthleteAccidentLocationEnum.SPORTCLUBSFACILITES, + label: + accidentLocation.professionalAthleteAccident + .atTheClubsSportsFacilites, + }, + { + value: + ProfessionalAthleteAccidentLocationEnum.TOORFROMTHESPORTCLUBSFACILITES, + label: + accidentLocation.professionalAthleteAccident + .toOrFromTheSportsFacilites, + }, + { + value: ProfessionalAthleteAccidentLocationEnum.OTHER, + label: accidentLocation.professionalAthleteAccident.other, + }, + ], + }), + ], + }), + // location of agriculture related accident + buildMultiField({ + id: 'accidentLocation.agricultureAccident', + title: accidentLocation.general.heading, + description: accidentLocation.general.description, + condition: (formValue) => isAgricultureAccident(formValue), + children: [ + buildRadioField({ + id: 'accidentLocation.answer', + title: '', + options: [ + { + value: AgricultureAccidentLocationEnum.ATTHEWORKPLACE, + label: accidentLocation.agricultureAccident.atTheWorkplace, + }, + { + value: AgricultureAccidentLocationEnum.TOORFROMTHEWORKPLACE, + label: accidentLocation.agricultureAccident.toOrFromTheWorkplace, + }, + { + value: AgricultureAccidentLocationEnum.OTHER, + label: accidentLocation.agricultureAccident.other, + }, + ], + }), + ], + }), + // Fisherman information only applicable to fisherman workplace accidents + buildMultiField({ + id: 'shipLocation.multifield', + title: accidentLocation.fishermanAccidentLocation.heading, + description: accidentLocation.fishermanAccidentLocation.description, + condition: (formValue) => isFishermanAccident(formValue), + children: [ + buildRadioField({ + id: 'shipLocation.answer', + title: '', + backgroundColor: 'blue', + options: [ + { + value: + FishermanWorkplaceAccidentShipLocationEnum.SAILINGORFISHING, + label: accidentLocation.fishermanAccidentLocation.whileSailing, + }, + { + value: FishermanWorkplaceAccidentShipLocationEnum.HARBOR, + label: accidentLocation.fishermanAccidentLocation.inTheHarbor, + }, + { + value: FishermanWorkplaceAccidentShipLocationEnum.OTHER, + label: accidentLocation.fishermanAccidentLocation.other, + }, + ], + }), + ], + }), + buildMultiField({ + id: 'locationAndPurpose', + title: locationAndPurpose.general.title, + description: locationAndPurpose.general.description, + condition: (formValue) => + !isFishermanAccident(formValue) && !hideLocationAndPurpose(formValue), + children: [ + buildTextField({ + id: 'locationAndPurpose.location', + title: locationAndPurpose.labels.location, + backgroundColor: 'blue', + variant: 'textarea', + required: true, + rows: 4, + maxLength: 2000, + }), + ], + }), + ], +}) diff --git a/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/aboutTheAccidentSection/rescueSquadInfoSubSection.ts b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/aboutTheAccidentSection/rescueSquadInfoSubSection.ts new file mode 100644 index 000000000000..640aa635f36b --- /dev/null +++ b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/aboutTheAccidentSection/rescueSquadInfoSubSection.ts @@ -0,0 +1,109 @@ +import { + buildCustomField, + buildDescriptionField, + buildMultiField, + buildSubSection, + buildTextField, +} from '@island.is/application/core' +import { representativeInfo, rescueSquadInfo } from '../../../lib/messages' +import { + isInjuredAndRepresentativeOfCompanyOrInstitute, + isReportingOnBehalfOfEmployee, + isRescueWorkAccident, +} from '../../../utils' + +// Rescue squad information when accident is related to rescue squad +export const rescueSquadInfoSubSection = buildSubSection({ + id: 'rescueSquadInfo.subSection', + title: rescueSquadInfo.general.title, + condition: (formValue) => + isRescueWorkAccident(formValue) && + !isReportingOnBehalfOfEmployee(formValue), + children: [ + buildMultiField({ + id: 'rescueSquad', + title: rescueSquadInfo.general.title, + description: rescueSquadInfo.general.description, + children: [ + buildTextField({ + id: 'companyInfo.name', + title: rescueSquadInfo.labels.name, + backgroundColor: 'blue', + width: 'half', + required: true, + maxLength: 100, + }), + buildTextField({ + id: 'companyInfo.nationalRegistrationId', + title: rescueSquadInfo.labels.nationalId, + backgroundColor: 'blue', + format: '######-####', + required: true, + width: 'half', + }), + buildDescriptionField({ + id: 'rescueSquadInfo.descriptionField', + description: rescueSquadInfo.labels.subDescription, + space: 'containerGutter', + titleVariant: 'h5', + title: rescueSquadInfo.labels.descriptionField, + condition: (formValue) => + !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), + }), + buildTextField({ + id: 'representative.name', + title: representativeInfo.labels.name, + backgroundColor: 'blue', + required: true, + width: 'half', + maxLength: 100, + condition: (formValue) => + !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), + }), + buildTextField({ + id: 'representative.nationalId', + title: representativeInfo.labels.nationalId, + backgroundColor: 'blue', + required: true, + width: 'half', + format: '######-####', + condition: (formValue) => + !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), + }), + buildTextField({ + id: 'representative.email', + title: representativeInfo.labels.email, + backgroundColor: 'blue', + variant: 'email', + width: 'half', + maxLength: 100, + required: true, + condition: (formValue) => + !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), + }), + buildTextField({ + id: 'representative.phoneNumber', + title: representativeInfo.labels.tel, + backgroundColor: 'blue', + format: '###-####', + variant: 'tel', + width: 'half', + doesNotRequireAnswer: true, + condition: (formValue) => + !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), + }), + buildCustomField( + { + id: 'representativeInfo.custom', + title: '', + doesNotRequireAnswer: true, + component: 'HiddenInformation', + }, + { + id: 'representativeInfo', + }, + ), + ], + }), + ], +}) diff --git a/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/aboutTheAccidentSection/schoolInfoSubSection.ts b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/aboutTheAccidentSection/schoolInfoSubSection.ts new file mode 100644 index 000000000000..148f9ec35872 --- /dev/null +++ b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/aboutTheAccidentSection/schoolInfoSubSection.ts @@ -0,0 +1,112 @@ +import { + buildCustomField, + buildDescriptionField, + buildMultiField, + buildSubSection, + buildTextField, +} from '@island.is/application/core' +import { representativeInfo, schoolInfo } from '../../../lib/messages' +import { + isInjuredAndRepresentativeOfCompanyOrInstitute, + isInternshipStudiesAccident, + isReportingOnBehalfOfEmployee, + isStudiesAccident, +} from '../../../utils' + +// School information if school accident +export const schoolInfoSubSection = buildSubSection({ + id: 'schoolInfo.subSection', + title: schoolInfo.general.title, + condition: (formValue) => + isStudiesAccident(formValue) && + !isInternshipStudiesAccident(formValue) && + !isReportingOnBehalfOfEmployee(formValue), + children: [ + buildMultiField({ + id: 'schoolInfo', + title: schoolInfo.general.title, + description: schoolInfo.general.description, + children: [ + buildTextField({ + id: 'companyInfo.name', + title: schoolInfo.labels.name, + backgroundColor: 'blue', + required: true, + width: 'half', + maxLength: 100, + }), + buildTextField({ + id: 'companyInfo.nationalRegistrationId', + title: schoolInfo.labels.nationalId, + backgroundColor: 'blue', + format: '######-####', + required: true, + width: 'half', + }), + buildDescriptionField({ + id: 'schoolInfo.descriptionField', + description: schoolInfo.labels.subDescription, + space: 'containerGutter', + titleVariant: 'h5', + title: schoolInfo.labels.descriptionField, + condition: (formValue) => + !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), + }), + // These should all be required if the user is not the representative of the company. + // Should look into if we can require conditionally + buildTextField({ + id: 'representative.name', + title: representativeInfo.labels.name, + backgroundColor: 'blue', + required: true, + width: 'half', + maxLength: 100, + condition: (formValue) => + !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), + }), + buildTextField({ + id: 'representative.nationalId', + title: representativeInfo.labels.nationalId, + backgroundColor: 'blue', + required: true, + width: 'half', + format: '######-####', + condition: (formValue) => + !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), + }), + buildTextField({ + id: 'representative.email', + title: representativeInfo.labels.email, + backgroundColor: 'blue', + variant: 'email', + width: 'half', + maxLength: 100, + required: true, + condition: (formValue) => + !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), + }), + buildTextField({ + id: 'representative.phoneNumber', + title: representativeInfo.labels.tel, + backgroundColor: 'blue', + format: '###-####', + variant: 'tel', + width: 'half', + condition: (formValue) => + !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), + }), + buildCustomField( + { + id: 'schoolInfo.custom', + title: '', + doesNotRequireAnswer: true, + component: 'HiddenInformation', + }, + { + id: 'representativeInfo', + }, + ), + ], + }), + ], +}) diff --git a/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/aboutTheAccidentSection/sportsClubInfoSubSection.ts b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/aboutTheAccidentSection/sportsClubInfoSubSection.ts new file mode 100644 index 000000000000..7914ecbe0158 --- /dev/null +++ b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/aboutTheAccidentSection/sportsClubInfoSubSection.ts @@ -0,0 +1,110 @@ +import { + buildCustomField, + buildDescriptionField, + buildMultiField, + buildSubSection, + buildTextField, +} from '@island.is/application/core' +import { + isInjuredAndRepresentativeOfCompanyOrInstitute, + isProfessionalAthleteAccident, + isReportingOnBehalfOfEmployee, +} from '../../../utils' +import { representativeInfo, sportsClubInfo } from '../../../lib/messages' + +// Sports club information when the injured has a sports related accident +export const sportsClubInfoSubSection = buildSubSection({ + id: 'sportsClubInfo.subSection', + title: sportsClubInfo.general.title, + condition: (formValue) => + isProfessionalAthleteAccident(formValue) && + !isReportingOnBehalfOfEmployee(formValue), + children: [ + buildMultiField({ + id: 'sportsClubInfo', + title: sportsClubInfo.general.title, + description: sportsClubInfo.general.description, + children: [ + buildTextField({ + id: 'companyInfo.name', + title: sportsClubInfo.labels.name, + backgroundColor: 'blue', + width: 'half', + required: true, + maxLength: 100, + }), + buildTextField({ + id: 'companyInfo.nationalRegistrationId', + title: sportsClubInfo.labels.nationalId, + backgroundColor: 'blue', + format: '######-####', + required: true, + width: 'half', + }), + buildDescriptionField({ + id: 'sportsClubInfo.descriptionField', + description: sportsClubInfo.labels.subDescription, + space: 'containerGutter', + titleVariant: 'h5', + title: sportsClubInfo.labels.descriptionField, + condition: (formValue) => + !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), + }), + // These should all be required if the user is not the representative of the company. + // Should look into if we can require conditionally + buildTextField({ + id: 'representative.name', + title: representativeInfo.labels.name, + backgroundColor: 'blue', + required: true, + width: 'half', + maxLength: 100, + condition: (formValue) => + !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), + }), + buildTextField({ + id: 'representative.nationalId', + title: representativeInfo.labels.nationalId, + backgroundColor: 'blue', + required: true, + width: 'half', + format: '######-####', + condition: (formValue) => + !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), + }), + buildTextField({ + id: 'representative.email', + title: representativeInfo.labels.email, + backgroundColor: 'blue', + variant: 'email', + width: 'half', + maxLength: 100, + required: true, + condition: (formValue) => + !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), + }), + buildTextField({ + id: 'representative.phoneNumber', + title: representativeInfo.labels.tel, + backgroundColor: 'blue', + format: '###-####', + variant: 'tel', + width: 'half', + condition: (formValue) => + !isInjuredAndRepresentativeOfCompanyOrInstitute(formValue), + }), + buildCustomField( + { + id: 'representativeInfo.custom', + title: '', + doesNotRequireAnswer: true, + component: 'HiddenInformation', + }, + { + id: 'representativeInfo', + }, + ), + ], + }), + ], +}) diff --git a/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/aboutTheAccidentSection/studiesAccidentSubSection.ts b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/aboutTheAccidentSection/studiesAccidentSubSection.ts new file mode 100644 index 000000000000..87bead5e5d5c --- /dev/null +++ b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/aboutTheAccidentSection/studiesAccidentSubSection.ts @@ -0,0 +1,41 @@ +import { + buildMultiField, + buildRadioField, + buildSubSection, +} from '@island.is/application/core' +import { accidentType } from '../../../lib/messages' +import { isStudiesAccident } from '../../../utils' +import { StudiesAccidentTypeEnum } from '../../../types' + +export const studiesAccidentSubSection = buildSubSection({ + id: 'studiesAccident.subSection', + title: accidentType.workAccidentType.subSectionTitle, + condition: (formValue) => isStudiesAccident(formValue), + children: [ + buildMultiField({ + id: 'studiesAccident.section', + title: accidentType.studiesAccidentType.heading, + description: accidentType.studiesAccidentType.description, + children: [ + buildRadioField({ + id: 'studiesAccident.type', + title: '', + options: [ + { + value: StudiesAccidentTypeEnum.INTERNSHIP, + label: accidentType.studiesAccidentType.internship, + }, + { + value: StudiesAccidentTypeEnum.APPRENTICESHIP, + label: accidentType.studiesAccidentType.apprenticeship, + }, + { + value: StudiesAccidentTypeEnum.VOCATIONALEDUCATION, + label: accidentType.studiesAccidentType.vocationalEducation, + }, + ], + }), + ], + }), + ], +}) diff --git a/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/aboutTheAccidentSection/workAccidentSubSection.ts b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/aboutTheAccidentSection/workAccidentSubSection.ts new file mode 100644 index 000000000000..bf47941d017b --- /dev/null +++ b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/aboutTheAccidentSection/workAccidentSubSection.ts @@ -0,0 +1,84 @@ +import { + buildAlertMessageField, + buildDescriptionField, + buildMultiField, + buildRadioField, + buildSubSection, + buildTextField, +} from '@island.is/application/core' +import { + accidentType, + attachments, + injuredPersonInformation, +} from '../../../lib/messages' +import { + isAgricultureAccident, + isReportingOnBehalfSelf, + isWorkAccident, +} from '../../../utils' +import { WorkAccidentTypeEnum } from '../../../types' + +export const workAccidentSubSection = buildSubSection({ + id: 'workAccident.subSection', + title: accidentType.workAccidentType.subSectionTitle, + condition: (formValue) => isWorkAccident(formValue), + children: [ + buildMultiField({ + id: 'workAccident.section', + title: accidentType.workAccidentType.heading, + description: accidentType.workAccidentType.description, + children: [ + buildRadioField({ + id: 'workAccident.type', + width: 'half', + title: '', + options: [ + { + value: WorkAccidentTypeEnum.GENERAL, + label: accidentType.workAccidentType.generalWorkAccident, + }, + { + value: WorkAccidentTypeEnum.FISHERMAN, + label: accidentType.workAccidentType.fishermanAccident, + }, + { + value: WorkAccidentTypeEnum.PROFESSIONALATHLETE, + label: accidentType.workAccidentType.professionalAthlete, + }, + { + value: WorkAccidentTypeEnum.AGRICULTURE, + label: accidentType.workAccidentType.agricultureAccident, + }, + ], + }), + buildAlertMessageField({ + id: 'attachments.injuryCertificate.alert2', + title: attachments.labels.alertMessage, + description: accidentType.warning.agricultureAccidentWarning, + doesNotRequireAnswer: true, + message: accidentType.warning.agricultureAccidentWarning, + alertType: 'warning', + condition: (formValue) => isAgricultureAccident(formValue), + marginBottom: 5, + }), + buildDescriptionField({ + id: 'workAccident.descriptionField', + space: 'containerGutter', + title: injuredPersonInformation.general.jobTitle, + description: injuredPersonInformation.general.jobTitleDescription, + width: 'full', + marginBottom: 2, + condition: (formValue) => isReportingOnBehalfSelf(formValue), + }), + buildTextField({ + id: 'workAccident.jobTitle', + title: injuredPersonInformation.labels.jobTitle, + backgroundColor: 'white', + width: 'full', + maxLength: 100, + condition: (formValue) => isReportingOnBehalfSelf(formValue), + }), + ], + }), + ], +}) diff --git a/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/aboutTheAccidentSection/workMachineSubSection.ts b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/aboutTheAccidentSection/workMachineSubSection.ts new file mode 100644 index 000000000000..6b9ccee5acdb --- /dev/null +++ b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/aboutTheAccidentSection/workMachineSubSection.ts @@ -0,0 +1,60 @@ +import { + buildMultiField, + buildRadioField, + buildSubSection, + buildTextField, +} from '@island.is/application/core' +import { application, workMachine } from '../../../lib/messages' +import { + isAgricultureAccident, + isGeneralWorkplaceAccident, +} from '../../../utils' +import { isSportAccidentAndEmployee } from '../../../utils/isSportAccidentAndEmployee' +import { NO, YES } from '../../../constants' + +// Workmachine information only applicable to generic workplace accidents +export const workMachineSubSection = buildSubSection({ + id: 'workMachine.section', + title: workMachine.general.sectionTitle, + condition: (formValue) => + isGeneralWorkplaceAccident(formValue) || + isAgricultureAccident(formValue) || + isSportAccidentAndEmployee(formValue), + children: [ + buildMultiField({ + id: 'workMachine', + title: workMachine.general.workMachineRadioTitle, + description: '', + children: [ + buildRadioField({ + id: 'workMachineRadio', + title: '', + backgroundColor: 'blue', + width: 'half', + required: true, + options: [ + { value: YES, label: application.general.yesOptionLabel }, + { value: NO, label: application.general.noOptionLabel }, + ], + }), + ], + }), + buildMultiField({ + id: 'workMachine.description', + title: workMachine.general.subSectionTitle, + condition: (formValue) => formValue.workMachineRadio === YES, + children: [ + buildTextField({ + id: 'workMachine.desriptionOfMachine', + title: workMachine.labels.desriptionOfMachine, + placeholder: workMachine.placeholder.desriptionOfMachine, + backgroundColor: 'blue', + rows: 4, + variant: 'textarea', + required: true, + maxLength: 2000, + }), + ], + }), + ], +}) diff --git a/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/externalDataSection.ts b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/externalDataSection.ts deleted file mode 100644 index 807c67e9c41d..000000000000 --- a/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/externalDataSection.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { - buildCustomField, - buildDataProviderItem, - buildExternalDataProvider, - buildMultiField, - buildSection, - buildSubSection, -} from '@island.is/application/core' -import { NationalRegistryUserApi } from '@island.is/application/types' -import { externalData } from '../../lib/messages' - -export const externalDataSection = buildSection({ - id: 'ExternalDataSection', - title: 'Meðferð á gögnum', - children: [ - buildMultiField({ - title: externalData.agreementDescription.sectionTitle, - id: 'agreementDescriptionMultiField', - space: 2, - children: [ - buildCustomField({ - id: 'agreementDescriptionCustomField', - title: '', - component: 'AgreementDescription', - doesNotRequireAnswer: true, - }), - buildCustomField( - { - id: 'extrainformationWithDataprovider', - title: '', - component: 'DescriptionWithLink', - doesNotRequireAnswer: true, - }, - { - descriptionFirstPart: externalData.extraInformation.description, - descriptionSecondPart: '', - linkName: externalData.extraInformation.linkText, - url: externalData.extraInformation.link, - }, - ), - ], - }), - buildSubSection({ - id: 'AccidentNotificationForm', - title: externalData.dataProvider.sectionTitle, - children: [ - buildExternalDataProvider({ - title: externalData.dataProvider.pageTitle, - id: 'approveExternalData', - subTitle: externalData.dataProvider.subTitle, - description: '', - checkboxLabel: externalData.dataProvider.checkboxLabel, - dataProviders: [ - buildDataProviderItem({ - id: 'directoryOfLabor', - title: externalData.directoryOfLabor.title, - subTitle: externalData.directoryOfLabor.description, - }), - buildDataProviderItem({ - id: 'revAndCustoms', - title: externalData.revAndCustoms.title, - subTitle: externalData.revAndCustoms.description, - }), - buildDataProviderItem({ - id: 'nationalInsurancy', - title: externalData.nationalInsurancy.title, - subTitle: externalData.nationalInsurancy.description, - }), - buildDataProviderItem({ - id: 'municipalCollectionAgency', - title: externalData.municipalCollectionAgency.title, - subTitle: externalData.municipalCollectionAgency.description, - }), - buildDataProviderItem({ - provider: NationalRegistryUserApi, - title: externalData.nationalRegistry.title, - subTitle: externalData.nationalRegistry.description, - }), - buildDataProviderItem({ - id: 'accidentProvider', - title: '', - subTitle: externalData.accidentProvider.description, - }), - ], - }), - ], - }), - ], -}) diff --git a/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/externalDataSection/accidentNotificationSubSection.ts b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/externalDataSection/accidentNotificationSubSection.ts new file mode 100644 index 000000000000..9551bdb5bad5 --- /dev/null +++ b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/externalDataSection/accidentNotificationSubSection.ts @@ -0,0 +1,53 @@ +import { + buildDataProviderItem, + buildExternalDataProvider, + buildSubSection, +} from '@island.is/application/core' +import { externalData } from '../../../lib/messages' +import { NationalRegistryUserApi } from '@island.is/application/types' + +export const accidentNotificationSubSection = buildSubSection({ + id: 'AccidentNotificationForm', + title: externalData.dataProvider.sectionTitle, + children: [ + buildExternalDataProvider({ + title: externalData.dataProvider.pageTitle, + id: 'approveExternalData', + subTitle: externalData.dataProvider.subTitle, + description: '', + checkboxLabel: externalData.dataProvider.checkboxLabel, + dataProviders: [ + buildDataProviderItem({ + id: 'directoryOfLabor', + title: externalData.directoryOfLabor.title, + subTitle: externalData.directoryOfLabor.description, + }), + buildDataProviderItem({ + id: 'revAndCustoms', + title: externalData.revAndCustoms.title, + subTitle: externalData.revAndCustoms.description, + }), + buildDataProviderItem({ + id: 'nationalInsurancy', + title: externalData.nationalInsurancy.title, + subTitle: externalData.nationalInsurancy.description, + }), + buildDataProviderItem({ + id: 'municipalCollectionAgency', + title: externalData.municipalCollectionAgency.title, + subTitle: externalData.municipalCollectionAgency.description, + }), + buildDataProviderItem({ + provider: NationalRegistryUserApi, + title: externalData.nationalRegistry.title, + subTitle: externalData.nationalRegistry.description, + }), + buildDataProviderItem({ + id: 'accidentProvider', + title: '', + subTitle: externalData.accidentProvider.description, + }), + ], + }), + ], +}) diff --git a/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/externalDataSection/agreementDescriptionMultiField.ts b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/externalDataSection/agreementDescriptionMultiField.ts new file mode 100644 index 000000000000..ff2088966a1e --- /dev/null +++ b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/externalDataSection/agreementDescriptionMultiField.ts @@ -0,0 +1,30 @@ +import { buildCustomField, buildMultiField } from '@island.is/application/core' +import { externalData } from '../../../lib/messages' + +export const agreementDescriptionMultiField = buildMultiField({ + title: externalData.agreementDescription.sectionTitle, + id: 'agreementDescriptionMultiField', + space: 2, + children: [ + buildCustomField({ + id: 'agreementDescriptionCustomField', + title: '', + component: 'AgreementDescription', + doesNotRequireAnswer: true, + }), + buildCustomField( + { + id: 'extrainformationWithDataprovider', + title: '', + component: 'DescriptionWithLink', + doesNotRequireAnswer: true, + }, + { + descriptionFirstPart: externalData.extraInformation.description, + descriptionSecondPart: '', + linkName: externalData.extraInformation.linkText, + url: externalData.extraInformation.link, + }, + ), + ], +}) diff --git a/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/externalDataSection/index.ts b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/externalDataSection/index.ts new file mode 100644 index 000000000000..da22cbd29167 --- /dev/null +++ b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/externalDataSection/index.ts @@ -0,0 +1,9 @@ +import { buildSection } from '@island.is/application/core' +import { agreementDescriptionMultiField } from './agreementDescriptionMultiField' +import { accidentNotificationSubSection } from './accidentNotificationSubSection' + +export const externalDataSection = buildSection({ + id: 'ExternalDataSection', + title: 'Meðferð á gögnum', + children: [agreementDescriptionMultiField, accidentNotificationSubSection], +}) diff --git a/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/index.ts b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/index.ts index f8b0408fe877..82a2dfd20d95 100644 --- a/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/index.ts +++ b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/index.ts @@ -3,13 +3,14 @@ import { Form, FormModes } from '@island.is/application/types' import Logo from '../../assets/Logo' import { application } from '../../lib/messages' -import { aboutTheAccidentSection } from './aboutTheAccidentSection' import { conclusionSection } from './conclusionSection' -import { externalDataSection } from './externalDataSection' + import { overviewSection } from './overviewSection' -import { whoIsTheNotificationForSection } from './whoIsTheNotificationForSection' import { betaTestSection } from './betaTestSection' import { applicantInformationSection } from './applicantInformationSection' +import { whoIsTheNotificationForSection } from './whoIsTheNotificationForSection' +import { externalDataSection } from './externalDataSection' +import { aboutTheAccidentSection } from './aboutTheAccidentSection' export const AccidentNotificationForm: Form = buildForm({ id: 'AccidentNotificationForm', diff --git a/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/whoIsTheNotificationForSection.ts b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/whoIsTheNotificationForSection.ts deleted file mode 100644 index 43b91fcd3f80..000000000000 --- a/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/whoIsTheNotificationForSection.ts +++ /dev/null @@ -1,277 +0,0 @@ -import { - buildAlertMessageField, - buildCheckboxField, - buildCustomField, - buildDescriptionField, - buildFileUploadField, - buildMultiField, - buildRadioField, - buildSection, - buildSubSection, - buildTextField, - getValueViaPath, -} from '@island.is/application/core' -import { FILE_SIZE_LIMIT, UPLOAD_ACCEPT, YES } from '../../constants' -import { - childInCustody, - injuredPersonInformation, - juridicalPerson, - powerOfAttorney, - whoIsTheNotificationFor, - error, -} from '../../lib/messages' -import { - PowerOfAttorneyUploadEnum, - WhoIsTheNotificationForEnum, -} from '../../types' -import { - isReportingOnBehalfOfChild, - isReportingOnBehalfOfEmployee, - isReportingOnBehalfOfInjured, -} from '../../utils' -import { isPowerOfAttorney } from '../../utils/isPowerOfAttorney' -import { isUploadNow } from '../../utils/isUploadNow' - -export const whoIsTheNotificationForSection = buildSection({ - id: 'whoIsTheNotificationFor.section', - title: whoIsTheNotificationFor.general.sectionTitle, - children: [ - buildMultiField({ - id: 'whoIsTheNotificationFor', - title: whoIsTheNotificationFor.general.heading, - description: whoIsTheNotificationFor.general.description, - children: [ - buildRadioField({ - id: 'whoIsTheNotificationFor.answer', - title: '', - width: 'half', - options: [ - { - value: WhoIsTheNotificationForEnum.ME, - label: whoIsTheNotificationFor.labels.me, - }, - { - value: WhoIsTheNotificationForEnum.POWEROFATTORNEY, - label: whoIsTheNotificationFor.labels.powerOfAttorney, - }, - { - value: WhoIsTheNotificationForEnum.JURIDICALPERSON, - label: whoIsTheNotificationFor.labels.juridicalPerson, - }, - { - value: WhoIsTheNotificationForEnum.CHILDINCUSTODY, - label: whoIsTheNotificationFor.labels.childInCustody, - }, - ], - }), - ], - }), - buildSubSection({ - id: 'injuredPersonInformation.section', - title: injuredPersonInformation.general.sectionTitle, - children: [ - buildMultiField({ - id: 'injuredPersonInformation', - title: injuredPersonInformation.general.heading, - description: (formValue) => - isReportingOnBehalfOfEmployee(formValue.answers) - ? injuredPersonInformation.general.juridicalDescription - : injuredPersonInformation.general.description, - children: [ - buildTextField({ - id: 'injuredPersonInformation.name', - title: injuredPersonInformation.labels.name, - width: 'half', - backgroundColor: 'blue', - required: true, - maxLength: 100, - }), - buildTextField({ - id: 'injuredPersonInformation.nationalId', - title: injuredPersonInformation.labels.nationalId, - format: '######-####', - width: 'half', - backgroundColor: 'blue', - required: true, - }), - buildTextField({ - id: 'injuredPersonInformation.email', - title: injuredPersonInformation.labels.email, - backgroundColor: 'blue', - width: 'half', - variant: 'email', - required: true, - maxLength: 100, - }), - buildTextField({ - id: 'injuredPersonInformation.phoneNumber', - title: injuredPersonInformation.labels.tel, - backgroundColor: 'blue', - format: '###-####', - width: 'half', - variant: 'tel', - }), - buildDescriptionField({ - id: 'accidentDetails.descriptionField', - space: 'containerGutter', - title: injuredPersonInformation.general.jobTitle, - description: injuredPersonInformation.general.jobTitleDescription, - width: 'full', - marginBottom: 2, - }), - buildTextField({ - id: 'injuredPersonInformation.jobTitle', - title: injuredPersonInformation.labels.jobTitle, - backgroundColor: 'white', - width: 'full', - maxLength: 100, - }), - ], - }), - ], - condition: (formValue) => isReportingOnBehalfOfInjured(formValue), - }), - buildSubSection({ - id: 'juridicalPerson.company', - title: juridicalPerson.general.sectionTitle, - children: [ - buildMultiField({ - id: 'juridicalPerson.company', - title: juridicalPerson.general.title, - description: juridicalPerson.general.description, - children: [ - buildTextField({ - id: 'juridicalPerson.companyName', - backgroundColor: 'blue', - title: juridicalPerson.labels.companyName, - width: 'half', - required: true, - maxLength: 100, - }), - buildTextField({ - id: 'juridicalPerson.companyNationalId', - backgroundColor: 'blue', - title: juridicalPerson.labels.companyNationalId, - format: '######-####', - width: 'half', - required: true, - }), - buildCheckboxField({ - id: 'juridicalPerson.companyConfirmation', - title: '', - large: false, - backgroundColor: 'white', - defaultValue: [], - options: [ - { - value: YES, - label: juridicalPerson.labels.confirmation, - }, - ], - }), - ], - }), - ], - condition: (formValue) => isReportingOnBehalfOfEmployee(formValue), - }), - buildSubSection({ - id: 'powerOfAttorney.type.section', - title: powerOfAttorney.type.sectionTitle, - children: [ - buildMultiField({ - id: 'powerOfAttorney.type.multifield', - title: powerOfAttorney.type.heading, - description: powerOfAttorney.type.description, - children: [ - buildRadioField({ - id: 'powerOfAttorney.type', - title: '', - options: [ - { - value: PowerOfAttorneyUploadEnum.UPLOADNOW, - label: powerOfAttorney.labels.uploadNow, - }, - { - value: PowerOfAttorneyUploadEnum.UPLOADLATER, - label: powerOfAttorney.labels.uploadLater, - }, - ], - }), - buildAlertMessageField({ - id: 'attachments.injuryCertificate.alert', - title: powerOfAttorney.alertMessage.title, - message: powerOfAttorney.alertMessage.description, - alertType: 'warning', - doesNotRequireAnswer: true, - condition: (formValue) => - getValueViaPath(formValue, 'powerOfAttorney.type') === - PowerOfAttorneyUploadEnum.UPLOADLATER, - }), - ], - }), - ], - condition: (formValue) => isPowerOfAttorney(formValue), - }), - buildSubSection({ - id: 'childInCustody.section', - title: childInCustody.general.sectionTitle, - children: [ - buildMultiField({ - id: 'childInCustody.fields', - title: childInCustody.general.screenTitle, - description: childInCustody.general.screenDescription, - children: [ - buildTextField({ - id: 'childInCustody.name', - backgroundColor: 'blue', - title: childInCustody.labels.name, - width: 'half', - required: true, - maxLength: 100, - }), - buildTextField({ - id: 'childInCustody.nationalId', - backgroundColor: 'blue', - title: childInCustody.labels.nationalId, - format: '######-####', - width: 'half', - required: true, - }), - ], - }), - ], - condition: (answers) => isReportingOnBehalfOfChild(answers), - }), - buildSubSection({ - id: 'powerOfAttorney.upload.section', - title: powerOfAttorney.upload.sectionTitle, - children: [ - buildMultiField({ - id: 'powerOfAttorney', - title: powerOfAttorney.upload.heading, - description: powerOfAttorney.upload.description, - children: [ - buildCustomField({ - id: 'attachments.powerOfAttorney.fileLink', - component: 'ProxyDocument', - doesNotRequireAnswer: true, - title: '', - }), - buildFileUploadField({ - id: 'attachments.powerOfAttorneyFile.file', - title: '', - introduction: '', - maxSize: FILE_SIZE_LIMIT, - maxSizeErrorText: error.attachmentMaxSizeError, - uploadAccept: UPLOAD_ACCEPT, - uploadHeader: powerOfAttorney.upload.uploadHeader, - uploadDescription: powerOfAttorney.upload.uploadDescription, - uploadButtonLabel: powerOfAttorney.upload.uploadButtonLabel, - }), - ], - }), - ], - condition: (formValue) => isUploadNow(formValue), - }), - ], -}) diff --git a/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/whoIsTheNotificationForSection/childInCustodySubSection.ts b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/whoIsTheNotificationForSection/childInCustodySubSection.ts new file mode 100644 index 000000000000..9587578fe81c --- /dev/null +++ b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/whoIsTheNotificationForSection/childInCustodySubSection.ts @@ -0,0 +1,38 @@ +import { + buildMultiField, + buildSubSection, + buildTextField, +} from '@island.is/application/core' +import { childInCustody } from '../../../lib/messages' +import { isReportingOnBehalfOfChild } from '../../../utils' + +export const childInCustodySubSection = buildSubSection({ + id: 'childInCustody.section', + title: childInCustody.general.sectionTitle, + children: [ + buildMultiField({ + id: 'childInCustody.fields', + title: childInCustody.general.screenTitle, + description: childInCustody.general.screenDescription, + children: [ + buildTextField({ + id: 'childInCustody.name', + backgroundColor: 'blue', + title: childInCustody.labels.name, + width: 'half', + required: true, + maxLength: 100, + }), + buildTextField({ + id: 'childInCustody.nationalId', + backgroundColor: 'blue', + title: childInCustody.labels.nationalId, + format: '######-####', + width: 'half', + required: true, + }), + ], + }), + ], + condition: (answers) => isReportingOnBehalfOfChild(answers), +}) diff --git a/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/whoIsTheNotificationForSection/index.ts b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/whoIsTheNotificationForSection/index.ts new file mode 100644 index 000000000000..cfd061cbe286 --- /dev/null +++ b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/whoIsTheNotificationForSection/index.ts @@ -0,0 +1,21 @@ +import { buildSection } from '@island.is/application/core' +import { whoIsTheNotificationFor } from '../../../lib/messages' +import { whoIsTheNotificationForMultiField } from './whoIsTheNotificationForMultiField' +import { injuredPersonInformationSubSection } from './injuredPersonInformationSubSection' +import { juridicalPersonCompanySubSection } from './juridicialPersonCompanySubSection' +import { powerOfAttorneySubSection } from './powerOfAttorneySubSection' +import { childInCustodySubSection } from './childInCustodySubSection' +import { powerOfAttorneyUploadSubSection } from './powerOfAttorneyUploadSubSection' + +export const whoIsTheNotificationForSection = buildSection({ + id: 'whoIsTheNotificationFor.section', + title: whoIsTheNotificationFor.general.sectionTitle, + children: [ + whoIsTheNotificationForMultiField, + injuredPersonInformationSubSection, + juridicalPersonCompanySubSection, + powerOfAttorneySubSection, + childInCustodySubSection, + powerOfAttorneyUploadSubSection, + ], +}) diff --git a/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/whoIsTheNotificationForSection/injuredPersonInformationSubSection.ts b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/whoIsTheNotificationForSection/injuredPersonInformationSubSection.ts new file mode 100644 index 000000000000..434a2b6c2ea1 --- /dev/null +++ b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/whoIsTheNotificationForSection/injuredPersonInformationSubSection.ts @@ -0,0 +1,77 @@ +import { + buildDescriptionField, + buildMultiField, + buildSubSection, + buildTextField, +} from '@island.is/application/core' +import { injuredPersonInformation } from '../../../lib/messages' +import { + isReportingOnBehalfOfEmployee, + isReportingOnBehalfOfInjured, +} from '../../../utils' + +export const injuredPersonInformationSubSection = buildSubSection({ + id: 'injuredPersonInformation.section', + title: injuredPersonInformation.general.sectionTitle, + children: [ + buildMultiField({ + id: 'injuredPersonInformation', + title: injuredPersonInformation.general.heading, + description: (formValue) => + isReportingOnBehalfOfEmployee(formValue.answers) + ? injuredPersonInformation.general.juridicalDescription + : injuredPersonInformation.general.description, + children: [ + buildTextField({ + id: 'injuredPersonInformation.name', + title: injuredPersonInformation.labels.name, + width: 'half', + backgroundColor: 'blue', + required: true, + maxLength: 100, + }), + buildTextField({ + id: 'injuredPersonInformation.nationalId', + title: injuredPersonInformation.labels.nationalId, + format: '######-####', + width: 'half', + backgroundColor: 'blue', + required: true, + }), + buildTextField({ + id: 'injuredPersonInformation.email', + title: injuredPersonInformation.labels.email, + backgroundColor: 'blue', + width: 'half', + variant: 'email', + required: true, + maxLength: 100, + }), + buildTextField({ + id: 'injuredPersonInformation.phoneNumber', + title: injuredPersonInformation.labels.tel, + backgroundColor: 'blue', + format: '###-####', + width: 'half', + variant: 'tel', + }), + buildDescriptionField({ + id: 'accidentDetails.descriptionField', + space: 'containerGutter', + title: injuredPersonInformation.general.jobTitle, + description: injuredPersonInformation.general.jobTitleDescription, + width: 'full', + marginBottom: 2, + }), + buildTextField({ + id: 'injuredPersonInformation.jobTitle', + title: injuredPersonInformation.labels.jobTitle, + backgroundColor: 'white', + width: 'full', + maxLength: 100, + }), + ], + }), + ], + condition: (formValue) => isReportingOnBehalfOfInjured(formValue), +}) diff --git a/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/whoIsTheNotificationForSection/juridicialPersonCompanySubSection.ts b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/whoIsTheNotificationForSection/juridicialPersonCompanySubSection.ts new file mode 100644 index 000000000000..45ec29a5a2a7 --- /dev/null +++ b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/whoIsTheNotificationForSection/juridicialPersonCompanySubSection.ts @@ -0,0 +1,53 @@ +import { + buildCheckboxField, + buildMultiField, + buildSubSection, + buildTextField, +} from '@island.is/application/core' +import { juridicalPerson } from '../../../lib/messages' +import { isReportingOnBehalfOfEmployee } from '../../../utils' +import { YES } from '../../../constants' + +export const juridicalPersonCompanySubSection = buildSubSection({ + id: 'juridicalPerson.company', + title: juridicalPerson.general.sectionTitle, + children: [ + buildMultiField({ + id: 'juridicalPerson.company', + title: juridicalPerson.general.title, + description: juridicalPerson.general.description, + children: [ + buildTextField({ + id: 'juridicalPerson.companyName', + backgroundColor: 'blue', + title: juridicalPerson.labels.companyName, + width: 'half', + required: true, + maxLength: 100, + }), + buildTextField({ + id: 'juridicalPerson.companyNationalId', + backgroundColor: 'blue', + title: juridicalPerson.labels.companyNationalId, + format: '######-####', + width: 'half', + required: true, + }), + buildCheckboxField({ + id: 'juridicalPerson.companyConfirmation', + title: '', + large: false, + backgroundColor: 'white', + defaultValue: [], + options: [ + { + value: YES, + label: juridicalPerson.labels.confirmation, + }, + ], + }), + ], + }), + ], + condition: (formValue) => isReportingOnBehalfOfEmployee(formValue), +}) diff --git a/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/whoIsTheNotificationForSection/powerOfAttorneySubSection.ts b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/whoIsTheNotificationForSection/powerOfAttorneySubSection.ts new file mode 100644 index 000000000000..eb16cefeec0f --- /dev/null +++ b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/whoIsTheNotificationForSection/powerOfAttorneySubSection.ts @@ -0,0 +1,49 @@ +import { + buildAlertMessageField, + buildMultiField, + buildRadioField, + buildSubSection, + getValueViaPath, +} from '@island.is/application/core' +import { powerOfAttorney } from '../../../lib/messages' +import { PowerOfAttorneyUploadEnum } from '../../../types' +import { isPowerOfAttorney } from '../../../utils' + +export const powerOfAttorneySubSection = buildSubSection({ + id: 'powerOfAttorney.type.section', + title: powerOfAttorney.type.sectionTitle, + children: [ + buildMultiField({ + id: 'powerOfAttorney.type.multifield', + title: powerOfAttorney.type.heading, + description: powerOfAttorney.type.description, + children: [ + buildRadioField({ + id: 'powerOfAttorney.type', + title: '', + options: [ + { + value: PowerOfAttorneyUploadEnum.UPLOADNOW, + label: powerOfAttorney.labels.uploadNow, + }, + { + value: PowerOfAttorneyUploadEnum.UPLOADLATER, + label: powerOfAttorney.labels.uploadLater, + }, + ], + }), + buildAlertMessageField({ + id: 'attachments.injuryCertificate.alert', + title: powerOfAttorney.alertMessage.title, + message: powerOfAttorney.alertMessage.description, + alertType: 'warning', + doesNotRequireAnswer: true, + condition: (formValue) => + getValueViaPath(formValue, 'powerOfAttorney.type') === + PowerOfAttorneyUploadEnum.UPLOADLATER, + }), + ], + }), + ], + condition: (formValue) => isPowerOfAttorney(formValue), +}) diff --git a/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/whoIsTheNotificationForSection/powerOfAttorneyUploadSubSection.ts b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/whoIsTheNotificationForSection/powerOfAttorneyUploadSubSection.ts new file mode 100644 index 000000000000..d3defa608ae5 --- /dev/null +++ b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/whoIsTheNotificationForSection/powerOfAttorneyUploadSubSection.ts @@ -0,0 +1,41 @@ +import { + buildCustomField, + buildFileUploadField, + buildMultiField, + buildSubSection, +} from '@island.is/application/core' +import { error, powerOfAttorney } from '../../../lib/messages' +import { FILE_SIZE_LIMIT, UPLOAD_ACCEPT } from '../../../constants' +import { isUploadNow } from '../../../utils/isUploadNow' + +export const powerOfAttorneyUploadSubSection = buildSubSection({ + id: 'powerOfAttorney.upload.section', + title: powerOfAttorney.upload.sectionTitle, + children: [ + buildMultiField({ + id: 'powerOfAttorney', + title: powerOfAttorney.upload.heading, + description: powerOfAttorney.upload.description, + children: [ + buildCustomField({ + id: 'attachments.powerOfAttorney.fileLink', + component: 'ProxyDocument', + doesNotRequireAnswer: true, + title: '', + }), + buildFileUploadField({ + id: 'attachments.powerOfAttorneyFile.file', + title: '', + introduction: '', + maxSize: FILE_SIZE_LIMIT, + maxSizeErrorText: error.attachmentMaxSizeError, + uploadAccept: UPLOAD_ACCEPT, + uploadHeader: powerOfAttorney.upload.uploadHeader, + uploadDescription: powerOfAttorney.upload.uploadDescription, + uploadButtonLabel: powerOfAttorney.upload.uploadButtonLabel, + }), + ], + }), + ], + condition: (formValue) => isUploadNow(formValue), +}) diff --git a/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/whoIsTheNotificationForSection/whoIsTheNotificationForMultiField.ts b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/whoIsTheNotificationForSection/whoIsTheNotificationForMultiField.ts new file mode 100644 index 000000000000..766ea3128bc2 --- /dev/null +++ b/libs/application/templates/accident-notification/src/forms/AccidentNotificationForm/whoIsTheNotificationForSection/whoIsTheNotificationForMultiField.ts @@ -0,0 +1,34 @@ +import { buildMultiField, buildRadioField } from '@island.is/application/core' +import { whoIsTheNotificationFor } from '../../../lib/messages' +import { WhoIsTheNotificationForEnum } from '../../../types' + +export const whoIsTheNotificationForMultiField = buildMultiField({ + id: 'whoIsTheNotificationFor', + title: whoIsTheNotificationFor.general.heading, + description: whoIsTheNotificationFor.general.description, + children: [ + buildRadioField({ + id: 'whoIsTheNotificationFor.answer', + title: '', + width: 'half', + options: [ + { + value: WhoIsTheNotificationForEnum.ME, + label: whoIsTheNotificationFor.labels.me, + }, + { + value: WhoIsTheNotificationForEnum.POWEROFATTORNEY, + label: whoIsTheNotificationFor.labels.powerOfAttorney, + }, + { + value: WhoIsTheNotificationForEnum.JURIDICALPERSON, + label: whoIsTheNotificationFor.labels.juridicalPerson, + }, + { + value: WhoIsTheNotificationForEnum.CHILDINCUSTODY, + label: whoIsTheNotificationFor.labels.childInCustody, + }, + ], + }), + ], +}) diff --git a/libs/application/templates/accident-notification/src/lib/dataSchema.ts b/libs/application/templates/accident-notification/src/lib/dataSchema.ts index 8777b3490c13..ea66619b0a67 100644 --- a/libs/application/templates/accident-notification/src/lib/dataSchema.ts +++ b/libs/application/templates/accident-notification/src/lib/dataSchema.ts @@ -20,6 +20,7 @@ import { } from '../types' import { isValid24HFormatTime } from '../utils' import { error } from './messages/error' +import { time } from 'console' export enum OnBehalf { MYSELF = 'myself', @@ -166,6 +167,14 @@ export const AccidentNotificationSchema = z.object({ accidentSymptoms: z.string().refine((x) => x.trim().length > 0, { params: error.invalidValue, }), + dateOfDoctorVisit: z.string().refine((x) => x.trim().length > 0, { + params: error.invalidValue, + }), + timeOfDoctorVisit: z + .string() + .refine((x) => (x ? isValid24HFormatTime(x) : false), { + params: error.invalidValue, + }), }), isRepresentativeOfCompanyOrInstitue: z.array(z.string()).optional(), fishingShipInfo: z.object({ @@ -252,6 +261,7 @@ export const AccidentNotificationSchema = z.object({ WorkAccidentTypeEnum.GENERAL, WorkAccidentTypeEnum.PROFESSIONALATHLETE, ]), + jobTitle: z.string().optional(), }), studiesAccident: z.object({ type: z.enum([ @@ -274,6 +284,7 @@ export const AccidentNotificationSchema = z.object({ params: error.invalidValue, }), phoneNumber: z.string().optional(), + jobTitle: z.string().optional(), }), juridicalPerson: z.object({ companyName: z.string().refine((x) => x.trim().length > 0, { diff --git a/libs/application/templates/accident-notification/src/lib/messages/injuredPersonInformation.ts b/libs/application/templates/accident-notification/src/lib/messages/injuredPersonInformation.ts index c2253673c238..c0136d511525 100644 --- a/libs/application/templates/accident-notification/src/lib/messages/injuredPersonInformation.ts +++ b/libs/application/templates/accident-notification/src/lib/messages/injuredPersonInformation.ts @@ -28,13 +28,13 @@ export const injuredPersonInformation = { jobTitle: { id: 'an.application:injuredPersonInformation.general.jobTitle', defaultMessage: 'Starfsheiti', - description: 'Job title', + description: 'Title above the job title input field', }, jobTitleDescription: { id: 'an.application:injuredPersonInformation.general.jobTitleDescription', defaultMessage: 'Sláðu inn starfsheiti þess slasaða þegar slysið átti sér stað.', - description: 'Description for job title', + description: 'Description for job title input field', }, }), labels: defineMessages({ @@ -61,7 +61,7 @@ export const injuredPersonInformation = { jobTitle: { id: 'an.application:injuredPersonInformation.labels.jobTitle', defaultMessage: 'Starfsheiti', - description: 'Job title', + description: 'Label for job title input field', }, }), upload: defineMessages({ diff --git a/libs/application/templates/financial-aid/src/fields/Summary/ChildrenInfo.tsx b/libs/application/templates/financial-aid/src/fields/Summary/ChildrenInfo.tsx index 8aa2596cf106..33fa1d31fe99 100644 --- a/libs/application/templates/financial-aid/src/fields/Summary/ChildrenInfo.tsx +++ b/libs/application/templates/financial-aid/src/fields/Summary/ChildrenInfo.tsx @@ -17,7 +17,6 @@ interface Props { } const ChildrenInfo = ({ childrenSchoolInfo, goToScreen }: Props) => { - const { formatMessage } = useIntl() return ( goToScreen?.(Routes.CHILDRENSCHOOLINFO)} @@ -50,7 +49,7 @@ const ChildInfo = ({ name, nationalId, school }: PropsInfo) => { const { formatMessage } = useIntl() return ( - + {formatMessage(summaryForm.childrenInfo.name)} @@ -58,8 +57,8 @@ const ChildInfo = ({ name, nationalId, school }: PropsInfo) => { {name} - - + + {formatMessage(summaryForm.childrenInfo.nationalId)} diff --git a/libs/application/templates/financial-aid/src/fields/Summary/SummaryForm.tsx b/libs/application/templates/financial-aid/src/fields/Summary/SummaryForm.tsx index 2a74bb29935d..a821406919fc 100644 --- a/libs/application/templates/financial-aid/src/fields/Summary/SummaryForm.tsx +++ b/libs/application/templates/financial-aid/src/fields/Summary/SummaryForm.tsx @@ -1,12 +1,13 @@ import React, { useMemo, useState } from 'react' import { useIntl } from 'react-intl' -import { Text, Box } from '@island.is/island-ui/core' +import { Text, Box, AlertMessage } from '@island.is/island-ui/core' import { getNextPeriod, estimatedBreakDown, aidCalculator, FamilyStatus, + ChildrenAid, } from '@island.is/financial-aid/shared/lib' import { useLocale } from '@island.is/localization' @@ -54,6 +55,10 @@ const SummaryForm = ({ application, goToScreen }: FAFieldBaseProps) => { } }, [externalData.municipality.data]) + const showAlertMessageAboutChildrenAid = + externalData.childrenCustodyInformation.data.length > 0 && + externalData.municipality.data?.childrenAid !== ChildrenAid.NOTDEFINED + return ( <> @@ -85,6 +90,33 @@ const SummaryForm = ({ application, goToScreen }: FAFieldBaseProps) => { )} + {showAlertMessageAboutChildrenAid && ( + + {externalData.municipality.data?.childrenAid === + ChildrenAid.APPLICANT ? ( + + {formatMessage(m.summaryForm.childrenAidAlert.aidGoesToUser)} + + } + /> + ) : ( + + {formatMessage( + m.summaryForm.childrenAidAlert.aidGoesToInstution, + )} + + } + /> + )} + + )} + diff --git a/libs/application/templates/financial-aid/src/lib/messages/summaryForm.ts b/libs/application/templates/financial-aid/src/lib/messages/summaryForm.ts index 31f05b187507..c1ff35c59550 100644 --- a/libs/application/templates/financial-aid/src/lib/messages/summaryForm.ts +++ b/libs/application/templates/financial-aid/src/lib/messages/summaryForm.ts @@ -45,6 +45,19 @@ export const summaryForm = { description: 'Summary form when fails to submit application', }, }), + childrenAidAlert: defineMessages({ + aidGoesToInstution: { + id: 'fa.application:section.summaryForm.childrenAidAlert.aidGoesToInstution', + defaultMessage: + 'Styrkur vegna barna er greiddur beint til viðeigandi skólastofnunar.', + description: 'Alert banner when children aid goes to instution', + }, + aidGoesToUser: { + id: 'fa.application:section.summaryForm.general.aidGoesToUser', + defaultMessage: 'Styrkur vegna barna bætist við þessa upphæð.', + description: 'Alert banner when children aid is paid from municipality', + }, + }), userInfo: defineMessages({ name: { id: 'fa.application:section.summaryForm.userInfo.name', diff --git a/libs/application/templates/inheritance-report/src/fields/CalculateShare/index.tsx b/libs/application/templates/inheritance-report/src/fields/CalculateShare/index.tsx index 18d71a4ddbf4..39a5ef0e8978 100644 --- a/libs/application/templates/inheritance-report/src/fields/CalculateShare/index.tsx +++ b/libs/application/templates/inheritance-report/src/fields/CalculateShare/index.tsx @@ -483,22 +483,6 @@ export const CalculateShare: FC> = ({ title={m.netProperty} value={roundedValueToNumber(netTotal)} /> - {deceasedHadAssets && shareTotal > 0 && ( - - )} - - - - - - - - - - - - - {deceasedWasInCohabitation && ( > = ({ value={roundedValueToNumber(estateTotal)} /> + {deceasedHadAssets && shareTotal > 0 && ( + + )} > = ({ export default CalculateShare -const ShareItemRow = ({ item }: { item: ShareItem }) => { - const { formatMessage } = useLocale() - - const total = item.items.reduce((acc, item) => acc + item.value, 0) - const shareTotal = item.items.reduce( - (acc, item) => acc + item.deceasedShareValue, - 0, - ) - - return ( - - - - {item.title && ( - {formatMessage(item.title)} - )} - - - - - {formatCurrency(String(roundedValueToNumber(total)))} - - - - {shareTotal > 0 && ( - <> - - {formatMessage(m.share)} - - - - - {formatCurrency(String(roundedValueToNumber(shareTotal)))} - - - - - )} - - - ) -} - const TitleRow = ({ title, value, diff --git a/libs/application/templates/inheritance-report/src/fields/CalculationsOfTotal/CalculateTotalDebts/index.tsx b/libs/application/templates/inheritance-report/src/fields/CalculationsOfTotal/CalculateTotalDebts/index.tsx index 69c580f28e22..83e7b2ba3f2c 100644 --- a/libs/application/templates/inheritance-report/src/fields/CalculationsOfTotal/CalculateTotalDebts/index.tsx +++ b/libs/application/templates/inheritance-report/src/fields/CalculationsOfTotal/CalculateTotalDebts/index.tsx @@ -21,8 +21,13 @@ export const CalculateTotalDebts: FC< const publicCharges = valueToNumber( getValueViaPath(answers, 'debts.publicCharges'), ) + const funeralCost = valueToNumber( + getValueViaPath(answers, 'funeralCost.total'), + ) - const [total] = useState(domesticAndForeignDebts + publicCharges) + const [total] = useState( + domesticAndForeignDebts + publicCharges + funeralCost, + ) useEffect(() => { setValue('debts.debtsTotal', total) diff --git a/libs/application/templates/inheritance-report/src/fields/HeirsAndPartitionRepeater/index.tsx b/libs/application/templates/inheritance-report/src/fields/HeirsAndPartitionRepeater/index.tsx index 683c383d634b..2820995282a1 100644 --- a/libs/application/templates/inheritance-report/src/fields/HeirsAndPartitionRepeater/index.tsx +++ b/libs/application/templates/inheritance-report/src/fields/HeirsAndPartitionRepeater/index.tsx @@ -120,7 +120,9 @@ export const HeirsAndPartitionRepeater: FC< label: relation, })) || [] - const error = (errors as any)?.heirs?.data ?? {} + const error = + ((errors as any)?.heirs?.data || (errors as any)?.heirs?.total) ?? [] + console.log(error) const handleAddMember = () => append({ @@ -617,7 +619,7 @@ export const HeirsAndPartitionRepeater: FC< readOnly hasError={ (props.sumField === 'heirsPercentage' && - error && + !!error.length && total !== 100) ?? false } diff --git a/libs/application/templates/inheritance-report/src/fields/OverviewAssets/rows.ts b/libs/application/templates/inheritance-report/src/fields/OverviewAssets/rows.ts index 94e36c9d2094..c6bd60bc60f5 100644 --- a/libs/application/templates/inheritance-report/src/fields/OverviewAssets/rows.ts +++ b/libs/application/templates/inheritance-report/src/fields/OverviewAssets/rows.ts @@ -89,7 +89,7 @@ export const getGunsDataRow = (answers: FormValue): RowType[] => { const items: RowItemsType = [ { - title: m.gunNumber, + title: m.gunSerialNumber, value: item.assetNumber?.toUpperCase() ?? '', }, ] diff --git a/libs/application/templates/inheritance-report/src/fields/ReportFieldsRepeater/index.tsx b/libs/application/templates/inheritance-report/src/fields/ReportFieldsRepeater/index.tsx index 5d4e91f9ee60..e7386e472552 100644 --- a/libs/application/templates/inheritance-report/src/fields/ReportFieldsRepeater/index.tsx +++ b/libs/application/templates/inheritance-report/src/fields/ReportFieldsRepeater/index.tsx @@ -352,10 +352,11 @@ export const ReportFieldsRepeater: FC< }} /> ) : field.type !== 'nationalId' && - field.id === 'assetNumber' ? ( + field.id === 'assetNumber' && + field.props?.assetKey === 'bankAccounts' ? ( { const description = [ - `${m.gunNumber.defaultMessage}: ${asset.assetNumber}`, + `${m.gunSerialNumber.defaultMessage}: ${asset.assetNumber}`, m.gunValuation.defaultMessage + ': ' + (asset.propertyValuation diff --git a/libs/application/templates/inheritance-report/src/forms/form.ts b/libs/application/templates/inheritance-report/src/forms/form.ts index 228232ffbce1..e8223fbbddaa 100644 --- a/libs/application/templates/inheritance-report/src/forms/form.ts +++ b/libs/application/templates/inheritance-report/src/forms/form.ts @@ -8,9 +8,8 @@ import { import { m } from '../lib/messages' import { DefaultEvents, Form, FormModes } from '@island.is/application/types' import { assets } from './sections/assets' -import { debts } from './sections/debts' +import { debtsAndFuneralCost } from './sections/debtsAndFuneralCost' import { heirs } from './sections/heirs' -import { funeralCost } from './sections/funeralCost' import { applicant } from './sections/applicant' import { dataCollection } from './sections/dataCollection' import { deceased } from './sections/deceased' @@ -26,13 +25,12 @@ export const form: Form = buildForm({ renderLastScreenButton: true, children: [ preSelection, - deceased, dataCollection, + deceased, applicationInfo, applicant, assets, - funeralCost, - debts, + debtsAndFuneralCost, heirs, buildSection({ id: 'finalStep', diff --git a/libs/application/templates/inheritance-report/src/forms/sections/assets.ts b/libs/application/templates/inheritance-report/src/forms/sections/assets.ts index 5940ab073a7f..a9f44ed29ca6 100644 --- a/libs/application/templates/inheritance-report/src/forms/sections/assets.ts +++ b/libs/application/templates/inheritance-report/src/forms/sections/assets.ts @@ -252,12 +252,15 @@ export const assets = buildSection({ { fields: [ { - title: m.gunNumber, + title: m.gunSerialNumber, id: 'assetNumber', + placeholder: 'VantarHér', + required: true, }, { title: m.gunType, id: 'description', + required: true, }, { title: m.gunValuation, diff --git a/libs/application/templates/inheritance-report/src/forms/sections/debts.ts b/libs/application/templates/inheritance-report/src/forms/sections/debts.ts deleted file mode 100644 index a6a85121e08a..000000000000 --- a/libs/application/templates/inheritance-report/src/forms/sections/debts.ts +++ /dev/null @@ -1,229 +0,0 @@ -import { - buildCustomField, - buildDescriptionField, - buildDividerField, - buildKeyValueField, - buildMultiField, - buildSection, - buildSubSection, - buildTextField, - getValueViaPath, -} from '@island.is/application/core' -import { Application } from '@island.is/application/types' -import { format as formatNationalId } from 'kennitala' -import { formatCurrency } from '@island.is/application/ui-components' -import { m } from '../../lib/messages' -import { AllDebts, ApplicationDebts } from '../../types' -import { getEstateDataFromApplication } from '../../lib/utils/helpers' - -export const debts = buildSection({ - id: 'debts', - title: m.debtsTitle, - children: [ - buildSubSection({ - id: 'domesticAndForeignDebts', - title: m.debtsTitle, - children: [ - buildMultiField({ - id: 'domesticAndForeignDebts', - title: m.debtsAndFuneralCost, - description: m.debtsAndFuneralCostDescription, - children: [ - buildDescriptionField({ - id: 'domesticAndForeignDebtsHeader', - title: m.domesticAndForeignDebts, - description: m.domesticAndForeignDebtsDescription, - titleVariant: 'h3', - }), - buildDescriptionField({ - id: 'debts.domesticAndForeignDebts.total', - title: '', - }), - buildCustomField( - { - title: '', - id: 'debts.domesticAndForeignDebts.data', - component: 'ReportFieldsRepeater', - }, - { - fields: [ - { - title: m.debtsCreditorName, - id: 'description', - }, - { - title: m.creditorsNationalId, - id: 'nationalId', - format: '######-####', - }, - { - title: m.debtsLoanIdentity, - id: 'assetNumber', - }, - { - title: m.debtsBalance, - id: 'propertyValuation', - required: true, - currency: true, - }, - ], - hideDeceasedShare: true, - repeaterButtonText: m.debtsRepeaterButton, - fromExternalData: 'otherDebts', - sumField: 'propertyValuation', - }, - ), - ], - }), - ], - }), - buildSubSection({ - id: 'publicCharges', - title: m.publicChargesTitle, - children: [ - buildMultiField({ - id: 'publicCharges', - title: m.debtsAndFuneralCost, - description: m.debtsAndFuneralCostDescription, - children: [ - buildDescriptionField({ - id: 'publicChargesHeader', - title: m.publicChargesTitle, - description: m.publicChargesDescription, - titleVariant: 'h3', - }), - buildDescriptionField({ - id: 'debts.publicCharges.total', - title: '', - }), - buildTextField({ - title: m.amount.defaultMessage, - id: 'debts.publicCharges', - width: 'half', - variant: 'currency', - defaultValue: (application: Application) => { - return ( - getEstateDataFromApplication(application) - ?.inheritanceReportInfo?.officialFees?.[0] - ?.propertyValuation ?? '0' - ) - }, - }), - ], - }), - ], - }), - buildSubSection({ - id: 'debtsOverview', - title: m.debtsOverview, - children: [ - buildMultiField({ - id: 'debtsOverview', - title: m.debtsOverview, - description: m.overviewDescription, - children: [ - buildDividerField({}), - buildDescriptionField({ - id: 'overviewDomesticAndForeignDebts', - title: m.debtsTitle, - titleVariant: 'h3', - marginBottom: 'gutter', - space: 'gutter', - }), - buildCustomField( - { - title: '', - id: 'estateDebtsCards', - component: 'Cards', - doesNotRequireAnswer: true, - }, - { - cards: ({ answers }: Application) => { - const allDebts = ( - answers.debts as unknown as ApplicationDebts - ).domesticAndForeignDebts.data - return ( - allDebts.map((debt: AllDebts) => ({ - title: debt.creditorName, - description: [ - `${m.nationalId.defaultMessage}: ${formatNationalId( - debt.nationalId ?? '', - )}`, - `${m.debtsLoanIdentity.defaultMessage}: ${ - debt.loanIdentity ?? '' - }`, - `${m.debtsBalance.defaultMessage}: ${formatCurrency( - debt.balance ?? '0', - )}`, - ], - })) ?? [] - ) - }, - }, - ), - buildKeyValueField({ - label: m.totalAmount, - display: 'flex', - value: ({ answers }) => - formatCurrency( - String( - getValueViaPath( - answers, - 'debts.domesticAndForeignDebts.total', - ), - ), - ), - }), - buildDividerField({}), - buildDescriptionField({ - id: 'overviewPublicCharges', - title: m.publicChargesTitle, - titleVariant: 'h3', - marginBottom: 'gutter', - space: 'gutter', - }), - buildCustomField( - { - title: '', - id: 'chargesCards', - component: 'Cards', - doesNotRequireAnswer: true, - }, - { - cards: ({ answers }: Application) => { - const publicCharges = ( - answers.debts as unknown as ApplicationDebts - ).publicCharges - return publicCharges - ? [ - { - title: m.publicChargesTitle.defaultMessage, - description: [`${formatCurrency(publicCharges)}`], - }, - ] - : [] - }, - }, - ), - buildKeyValueField({ - label: m.totalAmount, - display: 'flex', - value: ({ answers }) => { - const value = - getValueViaPath(answers, 'debts.publicCharges') || '0' - return formatCurrency(value) - }, - }), - buildDividerField({}), - buildCustomField({ - title: '', - id: 'debts.debtsTotal', - doesNotRequireAnswer: true, - component: 'CalculateTotalDebts', - }), - ], - }), - ], - }), - ], -}) diff --git a/libs/application/templates/inheritance-report/src/forms/sections/funeralCost.ts b/libs/application/templates/inheritance-report/src/forms/sections/debtsAndFuneralCost.ts similarity index 50% rename from libs/application/templates/inheritance-report/src/forms/sections/funeralCost.ts rename to libs/application/templates/inheritance-report/src/forms/sections/debtsAndFuneralCost.ts index d6f158f84ba2..59ab7668bb46 100644 --- a/libs/application/templates/inheritance-report/src/forms/sections/funeralCost.ts +++ b/libs/application/templates/inheritance-report/src/forms/sections/debtsAndFuneralCost.ts @@ -6,16 +6,103 @@ import { buildMultiField, buildSection, buildSubSection, + buildTextField, getValueViaPath, } from '@island.is/application/core' -import { m } from '../../lib/messages' +import { Application } from '@island.is/application/types' +import { format as formatNationalId } from 'kennitala' import { formatCurrency } from '@island.is/application/ui-components' +import { m } from '../../lib/messages' +import { ApplicationDebts, Debt } from '../../types' +import { getEstateDataFromApplication } from '../../lib/utils/helpers' import { YES } from '../../lib/constants' -export const funeralCost = buildSection({ - id: 'funeralCost', - title: m.funeralCostTitle, +export const debtsAndFuneralCost = buildSection({ + id: 'debts', + title: m.debtsAndFuneralCostTitle, children: [ + buildSubSection({ + id: 'domesticAndForeignDebts', + title: m.debtsTitle, + children: [ + buildMultiField({ + id: 'domesticAndForeignDebts', + title: m.debtsAndFuneralCost, + description: m.debtsAndFuneralCostDescription, + children: [ + buildDescriptionField({ + id: 'domesticAndForeignDebtsHeader', + title: m.domesticAndForeignDebts, + description: m.domesticAndForeignDebtsDescription, + titleVariant: 'h3', + }), + buildDescriptionField({ + id: 'debts.domesticAndForeignDebts.total', + title: '', + }), + buildCustomField( + { + title: '', + id: 'debts.domesticAndForeignDebts.data', + component: 'ReportFieldsRepeater', + }, + { + fields: [ + { + title: m.debtsCreditorName, + id: 'description', + }, + { + title: m.creditorsNationalId, + id: 'nationalId', + format: '######-####', + }, + { + title: m.debtsLoanIdentity, + id: 'assetNumber', + }, + { + title: m.debtsBalance, + id: 'propertyValuation', + required: true, + currency: true, + }, + ], + hideDeceasedShare: true, + repeaterButtonText: m.debtsRepeaterButton, + fromExternalData: 'otherDebts', + sumField: 'propertyValuation', + }, + ), + buildDescriptionField({ + id: 'publicChargesHeader', + title: m.publicChargesTitle, + description: m.publicChargesDescription, + titleVariant: 'h3', + space: 'containerGutter', + marginBottom: 'gutter', + }), + buildDescriptionField({ + id: 'debts.publicCharges.total', + title: '', + }), + buildTextField({ + title: m.amount.defaultMessage, + id: 'debts.publicCharges', + width: 'half', + variant: 'currency', + defaultValue: (application: Application) => { + return ( + getEstateDataFromApplication(application) + ?.inheritanceReportInfo?.officialFees?.[0] + ?.propertyValuation ?? '0' + ) + }, + }), + ], + }), + ], + }), buildSubSection({ id: 'funeralCost', title: m.funeralCostTitle, @@ -85,21 +172,116 @@ export const funeralCost = buildSection({ ], }), buildSubSection({ - id: 'funeralCostOverview', - title: m.overview, + id: 'debtsAndFuneralCostOverview', + title: m.debtsAndFuneralCostOverview, children: [ buildMultiField({ - id: 'funeralCostOverview', - title: m.overview, + id: 'debtsAndFuneralCostOverview', + title: m.debtsAndFuneralCostOverview, description: m.overviewDescription, children: [ + buildDividerField({}), buildDescriptionField({ - id: 'overviewFuneralCost', - title: m.funeralCostTitle, + id: 'overviewDomesticAndForeignDebts', + title: m.debtsTitle, titleVariant: 'h3', marginBottom: 'gutter', space: 'gutter', }), + buildCustomField( + { + title: '', + id: 'estateDebtsCards', + component: 'Cards', + doesNotRequireAnswer: true, + }, + { + cards: ({ answers }: Application) => { + const allDebts = ( + answers.debts as unknown as ApplicationDebts + ).domesticAndForeignDebts.data + return ( + allDebts.map((debt: Debt) => ({ + title: debt.description ?? '', + description: [ + `${m.nationalId.defaultMessage}: ${formatNationalId( + debt.nationalId ?? '', + )}`, + `${m.debtsLoanIdentity.defaultMessage}: ${ + debt.assetNumber ?? '' + }`, + `${m.debtsBalance.defaultMessage}: ${formatCurrency( + debt.propertyValuation ?? '0', + )}`, + ], + })) ?? [] + ) + }, + }, + ), + buildDividerField({}), + buildKeyValueField({ + label: m.totalAmountDebts, + display: 'flex', + value: ({ answers }) => + formatCurrency( + String( + getValueViaPath( + answers, + 'debts.domesticAndForeignDebts.total', + ), + ), + ), + }), + buildDividerField({}), + buildDescriptionField({ + id: 'overviewPublicCharges', + title: m.publicChargesTitle, + titleVariant: 'h3', + marginBottom: 'gutter', + space: 'gutter', + }), + buildCustomField( + { + title: '', + id: 'chargesCards', + component: 'Cards', + doesNotRequireAnswer: true, + }, + { + cards: ({ answers }: Application) => { + const publicCharges = ( + answers.debts as unknown as ApplicationDebts + ).publicCharges + return publicCharges + ? [ + { + title: m.publicChargesTitle.defaultMessage, + description: [`${formatCurrency(publicCharges)}`], + }, + ] + : [] + }, + }, + ), + buildDividerField({}), + buildKeyValueField({ + label: m.totalAmountPublic, + display: 'flex', + value: ({ answers }) => { + const value = + getValueViaPath(answers, 'debts.publicCharges') || '0' + return formatCurrency(value) + }, + }), + buildDividerField({}), + buildDescriptionField({ + id: 'overviewFuneralCost', + title: m.funeralCostTitle, + titleVariant: 'h3', + marginBottom: 'p3', + space: 'containerGutter', + }), buildKeyValueField({ label: m.funeralBuildCost, display: 'flex', @@ -215,13 +397,20 @@ export const funeralCost = buildSection({ }), buildDividerField({}), buildKeyValueField({ - label: m.totalAmount, + label: m.totalAmountFuneralCost, display: 'flex', value: ({ answers }) => formatCurrency( getValueViaPath(answers, 'funeralCost.total') || '0', ), }), + buildDividerField({}), + buildCustomField({ + title: '', + id: 'debts.debtsTotal', + doesNotRequireAnswer: true, + component: 'CalculateTotalDebts', + }), ], }), ], diff --git a/libs/application/templates/inheritance-report/src/forms/sections/deceased.ts b/libs/application/templates/inheritance-report/src/forms/sections/deceased.ts index ff7316c0811e..a4bc9780f3ff 100644 --- a/libs/application/templates/inheritance-report/src/forms/sections/deceased.ts +++ b/libs/application/templates/inheritance-report/src/forms/sections/deceased.ts @@ -10,6 +10,7 @@ import { m } from '../../lib/messages' import format from 'date-fns/format' import { getEstateDataFromApplication } from '../../lib/utils/helpers' import { NO, YES } from '../../lib/constants' +import { format as formatNationalId } from 'kennitala' export const deceased = buildSection({ id: 'deceasedInfo', @@ -30,8 +31,10 @@ export const deceased = buildSection({ buildKeyValueField({ label: m.nationalId, value: (application) => - getEstateDataFromApplication(application)?.inheritanceReportInfo - ?.nationalId ?? '', + formatNationalId( + getEstateDataFromApplication(application)?.inheritanceReportInfo + ?.nationalId ?? '', + ), width: 'half', }), buildDescriptionField({ diff --git a/libs/application/templates/inheritance-report/src/forms/sections/heirs.ts b/libs/application/templates/inheritance-report/src/forms/sections/heirs.ts index 619617b806ea..6f5438b4696e 100644 --- a/libs/application/templates/inheritance-report/src/forms/sections/heirs.ts +++ b/libs/application/templates/inheritance-report/src/forms/sections/heirs.ts @@ -3,6 +3,7 @@ import { buildCustomField, buildDescriptionField, buildDividerField, + buildFileUploadField, buildKeyValueField, buildMultiField, buildSection, @@ -165,12 +166,48 @@ export const heirs = buildSection({ title: m.heirAdditionalInfo, description: m.heirAdditionalInfoDescription, children: [ + buildDescriptionField({ + id: 'heirsAdditionalInfoFiles', + title: m.info, + titleVariant: 'h5', + marginBottom: 'smallGutter', + }), buildTextField({ id: 'heirsAdditionalInfo', - title: m.info, + title: '', placeholder: m.infoPlaceholder, variant: 'textarea', - rows: 7, + rows: 5, + maxLength: 1800, + }), + buildDescriptionField({ + id: 'heirsAdditionalInfoFiles', + title: m.fileUploadPrivateTransfer, + titleVariant: 'h5', + space: 'containerGutter', + marginBottom: 'smallGutter', + }), + buildFileUploadField({ + id: 'heirsAdditionalInfoPrivateTransferFiles', + uploadMultiple: false, + title: '', + description: '', + uploadHeader: '', + uploadDescription: '', + }), + buildDescriptionField({ + id: 'heirsAdditionalInfoFiles', + title: m.fileUploadOtherDocuments, + titleVariant: 'h5', + space: 'containerGutter', + marginBottom: 'smallGutter', + }), + buildFileUploadField({ + id: 'heirsAdditionalInfoFiles', + title: '', + description: '', + uploadHeader: '', + uploadDescription: '', }), ], }), diff --git a/libs/application/templates/inheritance-report/src/forms/sections/preSelection.ts b/libs/application/templates/inheritance-report/src/forms/sections/preSelection.ts index 2b2354c02615..a5573c3f8b50 100644 --- a/libs/application/templates/inheritance-report/src/forms/sections/preSelection.ts +++ b/libs/application/templates/inheritance-report/src/forms/sections/preSelection.ts @@ -34,7 +34,7 @@ export const preSelection = buildSection({ children: [ buildSelectField({ id: 'estateInfoSelection', - title: m.preDataCollectionChooseEstateSelectTitle, + title: m.preDataCollectionChooseEstateSelectTitleDropdown, defaultValue: (application: { externalData: { syslumennOnEntry: { diff --git a/libs/application/templates/inheritance-report/src/lib/messages.ts b/libs/application/templates/inheritance-report/src/lib/messages.ts index 8aa6d9515554..6f996b7fff4e 100644 --- a/libs/application/templates/inheritance-report/src/lib/messages.ts +++ b/libs/application/templates/inheritance-report/src/lib/messages.ts @@ -27,7 +27,12 @@ export const m = defineMessages({ preDataCollectionChooseEstateSelectTitle: { id: 'ir.application:preDataCollectionChooseEstateSelectTitle', - defaultMessage: 'Foröflun gagna', + defaultMessage: 'Upplýsingaöflun', + description: 'Title for pre-collection of data', + }, + preDataCollectionChooseEstateSelectTitleDropdown: { + id: 'ir.application:preDataCollectionChooseEstateSelectTitleDropdown', + defaultMessage: 'Upplýsingaöflun (nýr textareitur)', description: 'Title for pre-collection of data', }, @@ -421,7 +426,7 @@ export const m = defineMessages({ description: '', }, propertyNumber: { - id: 'es.application:propertyNumber', + id: 'ir.application:propertyNumber', defaultMessage: 'Fastanúmer', description: 'Property number label', }, @@ -487,8 +492,8 @@ export const m = defineMessages({ defaultMessage: 'Markaðsverðmæti á dánardegi', description: '', }, - gunNumber: { - id: 'ir.application:gunNumber', + gunSerialNumber: { + id: 'ir.application:gunSerialNumber', defaultMessage: 'Raðnúmer', description: '', }, @@ -711,9 +716,9 @@ export const m = defineMessages({ defaultMessage: 'Yfirlit eigna', description: '', }, - debtsOverview: { - id: 'ir.application:debtsOverview', - defaultMessage: 'Yfirlit skulda', + debtsAndFuneralCostOverview: { + id: 'ir.application:debtsAndFuneralCostOverview', + defaultMessage: 'Yfirlit', description: '', }, assetOverviewDescription: { @@ -934,6 +939,30 @@ export const m = defineMessages({ description: '', }, + totalAmountDebts: { + id: 'ir.application:totalAmountDebts', + defaultMessage: 'Samtals fjárhæð skuldir', + description: '', + }, + + totalAmountPublic: { + id: 'ir.application:totalAmountPublic', + defaultMessage: 'Samtals fjárhæð opinber gjöld', + description: '', + }, + + totalAmountFuneralCost: { + id: 'ir.application:totalAmountFuneralCost', + defaultMessage: 'Samtals fjárhæð útfararkostnaðar', + description: '', + }, + + debtsAndFuneralCostTitle: { + id: 'ir.application:debtsAndFuneralCostTitle', + defaultMessage: 'Skuldir og útfararkostnaður', + description: '', + }, + // Business business: { id: 'ir.application:business', @@ -1356,6 +1385,16 @@ export const m = defineMessages({ defaultMessage: 'Athugasemdir erfingja', description: '', }, + fileUploadPrivateTransfer: { + id: 'ir.application:fileUploadPrivateTransfer', + defaultMessage: 'Einkaskiptagerð', + description: '', + }, + fileUploadOtherDocuments: { + id: 'ir.application:fileUploadOtherDocuments', + defaultMessage: 'Önnur fylgigögn', + description: '', + }, heirShare: { id: 'ir.application:heirShare', defaultMessage: 'Arfur og erfðafjárskattur', @@ -1468,7 +1507,7 @@ export const m = defineMessages({ }, // Error messages errorPropertyNumber: { - id: 'es.application:error.errorPropertyNumber', + id: 'ir.application:error.errorPropertyNumber', defaultMessage: 'Verður að innihalda 6 tölustafi eða L + 6 fyrir landeignanúmeri, 7 tölustafi, F + 7 fyrir fasteignanúmeri', description: 'Property number is invalid', diff --git a/libs/application/templates/inheritance-report/src/types.ts b/libs/application/templates/inheritance-report/src/types.ts index 3c9d75276804..1b4c7ba570d9 100644 --- a/libs/application/templates/inheritance-report/src/types.ts +++ b/libs/application/templates/inheritance-report/src/types.ts @@ -210,12 +210,11 @@ export interface BankAccounts { total: number } -export interface AllDebts { - balance: string +export interface Debt { + assetNumber: string nationalId: string - creditorName: string - loanIdentity: string - taxFreeInheritance: number + description: string + propertyValuation: string } export interface ApplicationDebts { @@ -232,7 +231,7 @@ interface DomesticAndForeignDebtsData { } interface DomesticAndForeignDebts { - data: DomesticAndForeignDebtsData[] + data: Debt[] total: number } diff --git a/libs/application/templates/parental-leave/src/fields/FirstPeriodStart/index.tsx b/libs/application/templates/parental-leave/src/fields/FirstPeriodStart/index.tsx index 866b974fa322..59d8b6d405dc 100644 --- a/libs/application/templates/parental-leave/src/fields/FirstPeriodStart/index.tsx +++ b/libs/application/templates/parental-leave/src/fields/FirstPeriodStart/index.tsx @@ -1,6 +1,5 @@ import React, { FC, useState, useEffect } from 'react' import { useFormContext } from 'react-hook-form' - import { extractRepeaterIndexFromField } from '@island.is/application/core' import { FieldBaseProps } from '@island.is/application/types' import { Box } from '@island.is/island-ui/core' @@ -9,13 +8,12 @@ import { RadioController, } from '@island.is/shared/form-fields' import { useLocale } from '@island.is/localization' - import { - getExpectedDateOfBirthOrAdoptionDate, getApplicationAnswers, getBeginningOfThisMonth, isParentalGrant, isFosterCareAndAdoption, + getExpectedDateOfBirthOrAdoptionDateOrBirthDate, } from '../../lib/parentalLeaveUtils' import { parentalLeaveFormMessages } from '../../lib/messages' import { StartDateOptions } from '../../constants' @@ -30,15 +28,17 @@ const FirstPeriodStart: FC> = ({ const { register, unregister, setValue } = useFormContext() const { formatMessage } = useLocale() const expectedDateOfBirthOrAdoptionDate = - getExpectedDateOfBirthOrAdoptionDate(application) + getExpectedDateOfBirthOrAdoptionDateOrBirthDate(application) + const expectedDateOfBirthOrAdoptionDateOrBirthDate = + getExpectedDateOfBirthOrAdoptionDateOrBirthDate(application, true) const { rawPeriods } = getApplicationAnswers(application.answers) const currentIndex = extractRepeaterIndexFromField(field) const currentPeriod = rawPeriods[currentIndex] let isDisable = true - if (expectedDateOfBirthOrAdoptionDate) { + if (expectedDateOfBirthOrAdoptionDateOrBirthDate) { const expectedDateTime = new Date( - expectedDateOfBirthOrAdoptionDate, + expectedDateOfBirthOrAdoptionDateOrBirthDate, ).getTime() const beginningOfMonth = getBeginningOfThisMonth() const today = new Date() @@ -160,9 +160,10 @@ const FirstPeriodStart: FC> = ({ type="hidden" value={ statefulAnswer === StartDateOptions.ESTIMATED_DATE_OF_BIRTH || - statefulAnswer === StartDateOptions.ACTUAL_DATE_OF_BIRTH || statefulAnswer === StartDateOptions.ADOPTION_DATE ? expectedDateOfBirthOrAdoptionDate + : statefulAnswer === StartDateOptions.ACTUAL_DATE_OF_BIRTH + ? expectedDateOfBirthOrAdoptionDateOrBirthDate : undefined } {...register(startDateFieldId)} diff --git a/libs/application/templates/parental-leave/src/fields/InReviewSteps/index.tsx b/libs/application/templates/parental-leave/src/fields/InReviewSteps/index.tsx index 1f5391dcaa50..f5bbdb77ff19 100644 --- a/libs/application/templates/parental-leave/src/fields/InReviewSteps/index.tsx +++ b/libs/application/templates/parental-leave/src/fields/InReviewSteps/index.tsx @@ -11,11 +11,16 @@ import { Review } from '../Review/Review' import { parentalLeaveFormMessages } from '../../lib/messages' import { getApplicationAnswers, - getExpectedDateOfBirthOrAdoptionDate, + getExpectedDateOfBirthOrAdoptionDateOrBirthDate, isFosterCareAndAdoption, showResidenceGrant, } from '../../lib/parentalLeaveUtils' -import { States as ApplicationStates, States, YES } from '../../constants' +import { + States as ApplicationStates, + StartDateOptions, + States, + YES, +} from '../../constants' import { useRemainingRights } from '../../hooks/useRemainingRights' const InReviewSteps: FC> = (props) => { @@ -32,7 +37,7 @@ const InReviewSteps: FC> = (props) => { ) const { formatMessage } = useLocale() - const dob = getExpectedDateOfBirthOrAdoptionDate(application) + const dob = getExpectedDateOfBirthOrAdoptionDateOrBirthDate(application, true) const dobDate = dob ? new Date(dob) : null const canBeEdited = @@ -104,6 +109,9 @@ const InReviewSteps: FC> = (props) => { ? formatMessage( parentalLeaveFormMessages.reviewScreen.adoptionDate, ) + : periods?.[0]?.firstPeriodStart === + StartDateOptions.ACTUAL_DATE_OF_BIRTH + ? formatMessage(parentalLeaveFormMessages.shared.dateOfBirthTitle) : formatMessage( parentalLeaveFormMessages.reviewScreen.estimatedBirthDate, )} diff --git a/libs/application/templates/parental-leave/src/fields/PeriodsRepeater/index.tsx b/libs/application/templates/parental-leave/src/fields/PeriodsRepeater/index.tsx index ed89739f2eb2..cdf8a5334ea6 100644 --- a/libs/application/templates/parental-leave/src/fields/PeriodsRepeater/index.tsx +++ b/libs/application/templates/parental-leave/src/fields/PeriodsRepeater/index.tsx @@ -1,6 +1,5 @@ import React, { FC, useEffect } from 'react' import { useMutation, useQuery } from '@apollo/client' - import { UPDATE_APPLICATION } from '@island.is/application/graphql' import { RepeaterProps, FieldBaseProps } from '@island.is/application/types' import { @@ -17,14 +16,13 @@ import { } from '@island.is/shared/problem' import { useLocale } from '@island.is/localization' import { FieldDescription } from '@island.is/shared/form-fields' - import { Timeline } from '../components/Timeline/Timeline' import { formatPeriods, getAvailableRightsInDays, - getExpectedDateOfBirthOrAdoptionDate, getApplicationAnswers, synchronizeVMSTPeriods, + getExpectedDateOfBirthOrAdoptionDateOrBirthDate, } from '../../lib/parentalLeaveUtils' import { errorMessages, parentalLeaveFormMessages } from '../../lib/messages' import { States } from '../../constants' @@ -60,7 +58,7 @@ const PeriodsRepeater: FC> = ({ application.state === States.EDIT_OR_ADD_EMPLOYERS_AND_PERIODS const showDescription = field?.props?.showDescription ?? true - const dob = getExpectedDateOfBirthOrAdoptionDate(application) + const dob = getExpectedDateOfBirthOrAdoptionDateOrBirthDate(application, true) const { formatMessage, locale } = useLocale() const rights = getAvailableRightsInDays(application) const daysAlreadyUsed = useDaysAlreadyUsed(application) diff --git a/libs/application/templates/parental-leave/src/lib/answerValidationSections/utils.ts b/libs/application/templates/parental-leave/src/lib/answerValidationSections/utils.ts index 2a5d9a79c195..53118f62e143 100644 --- a/libs/application/templates/parental-leave/src/lib/answerValidationSections/utils.ts +++ b/libs/application/templates/parental-leave/src/lib/answerValidationSections/utils.ts @@ -19,10 +19,7 @@ import { NO, MINIMUM_PERIOD_LENGTH, } from '../../constants' -import { - getApplicationExternalData, - getExpectedDateOfBirthOrAdoptionDate, -} from '../parentalLeaveUtils' +import { getExpectedDateOfBirthOrAdoptionDateOrBirthDate } from '../parentalLeaveUtils' import { minimumPeriodStartBeforeExpectedDateOfBirth, minimumRatio, @@ -124,13 +121,20 @@ export const validatePeriod = ( ) => AnswerValidationError, ) => { const expectedDateOfBirthOrAdoptionDate = - getExpectedDateOfBirthOrAdoptionDate(application) - - if (!expectedDateOfBirthOrAdoptionDate) { + getExpectedDateOfBirthOrAdoptionDateOrBirthDate(application) + const expectedDateOfBirthOrAdoptionDateOrBirthDate = + getExpectedDateOfBirthOrAdoptionDateOrBirthDate(application, true) + + if ( + !expectedDateOfBirthOrAdoptionDate || + !expectedDateOfBirthOrAdoptionDateOrBirthDate + ) { return buildError(null, errorMessages.dateOfBirth) } - const dob = parseISO(expectedDateOfBirthOrAdoptionDate) + const dob = StartDateOptions.ACTUAL_DATE_OF_BIRTH + ? parseISO(expectedDateOfBirthOrAdoptionDateOrBirthDate) + : parseISO(expectedDateOfBirthOrAdoptionDate) const today = new Date() const minimumStartDate = addMonths( dob, @@ -165,20 +169,12 @@ export const validatePeriod = ( return buildError('startDate', errorMessages.periodsStartMissing) } else if (hasBeenAnswered(startDate)) { if (isFirstPeriod && parseISO(startDate) > today) { - if (firstPeriodStart === StartDateOptions.ACTUAL_DATE_OF_BIRTH) { - const { dateOfBirth } = getApplicationExternalData( - application.externalData, - ) - const dateOB = dateOfBirth?.data?.dateOfBirth - startDateValue = dateOB ? parseISO(dateOB) : dob - } else if ( + startDateValue = + firstPeriodStart === StartDateOptions.ACTUAL_DATE_OF_BIRTH || firstPeriodStart === StartDateOptions.ESTIMATED_DATE_OF_BIRTH || firstPeriodStart === StartDateOptions.ADOPTION_DATE - ) { - startDateValue = dob - } else { - startDateValue = parseISO(startDate) - } + ? dob + : parseISO(startDate) } else { startDateValue = parseISO(startDate) } diff --git a/libs/application/templates/parental-leave/src/lib/answerValidationSections/validateLatestPeriodValidationSection.ts b/libs/application/templates/parental-leave/src/lib/answerValidationSections/validateLatestPeriodValidationSection.ts index f95def28253a..85e20a4b2afa 100644 --- a/libs/application/templates/parental-leave/src/lib/answerValidationSections/validateLatestPeriodValidationSection.ts +++ b/libs/application/templates/parental-leave/src/lib/answerValidationSections/validateLatestPeriodValidationSection.ts @@ -11,7 +11,7 @@ import { calculateDaysUsedByPeriods, filterValidPeriods, getAvailableRightsInDays, - getExpectedDateOfBirthOrAdoptionDate, + getExpectedDateOfBirthOrAdoptionDateOrBirthDate, } from '../parentalLeaveUtils' import { ValidateField, @@ -121,10 +121,10 @@ export const validateLatestPeriodValidationSection = ( const latestPeriodIndex = periods.length - 1 const latestPeriod = periods[latestPeriodIndex] - const expectedDateOfBirthOrAdoptionDate = - getExpectedDateOfBirthOrAdoptionDate(application) + const expectedDateOfBirthOrAdoptionDateOrBirthDate = + getExpectedDateOfBirthOrAdoptionDateOrBirthDate(application, true) - if (!expectedDateOfBirthOrAdoptionDate) { + if (!expectedDateOfBirthOrAdoptionDateOrBirthDate) { return { path: 'periods', message: errorMessages.dateOfBirth, diff --git a/libs/application/templates/parental-leave/src/lib/parentalLeaveUtils.spec.ts b/libs/application/templates/parental-leave/src/lib/parentalLeaveUtils.spec.ts index 301dd9ed598e..648069844faa 100644 --- a/libs/application/templates/parental-leave/src/lib/parentalLeaveUtils.spec.ts +++ b/libs/application/templates/parental-leave/src/lib/parentalLeaveUtils.spec.ts @@ -26,7 +26,7 @@ import { ChildInformation } from '../dataProviders/Children/types' import { formatIsk, getAvailableRightsInMonths, - getExpectedDateOfBirthOrAdoptionDate, + getExpectedDateOfBirthOrAdoptionDateOrBirthDate, getSelectedChild, getTransferredDays, getOtherParentId, @@ -109,10 +109,10 @@ const createApplicationBase = (): Application => ({ status: ApplicationStatus.IN_PROGRESS, }) -describe('getExpectedDateOfBirthOrAdoptionDate', () => { +describe('getExpectedDateOfBirthOrAdoptionDateOrBirthDate', () => { it('should return undefined when no child is found', () => { const application = buildApplication() - const res = getExpectedDateOfBirthOrAdoptionDate(application) + const res = getExpectedDateOfBirthOrAdoptionDateOrBirthDate(application) expect(res).toBeUndefined() }) @@ -142,10 +142,50 @@ describe('getExpectedDateOfBirthOrAdoptionDate', () => { }, }) - const res = getExpectedDateOfBirthOrAdoptionDate(application) + const res = getExpectedDateOfBirthOrAdoptionDateOrBirthDate(application) expect(res).toEqual('2021-05-17') }) + + it('should return the selected child DOB', () => { + const application = buildApplication({ + answers: { + selectedChild: 0, + }, + externalData: { + children: { + data: { + children: [ + { + hasRights: true, + remainingDays: 180, + transferredDays: undefined, // Transferred days are only defined for secondary parents + parentalRelation: ParentalRelations.primary, + expectedDateOfBirth: '2021-05-17', + }, + ], + existingApplications: [], + }, + date: new Date(), + status: 'success', + }, + dateOfBirth: { + data: { + dateOfBirth: '2021-05-10', + }, + date: new Date(), + status: 'success', + }, + }, + }) + + const res = getExpectedDateOfBirthOrAdoptionDateOrBirthDate( + application, + true, + ) + + expect(res).toEqual('2021-05-10') + }) }) describe('isFosterCareAndAdoption', () => { @@ -1544,7 +1584,7 @@ test.each([ }, ) -describe.only('getActionName', () => { +describe('getActionName', () => { let application: Application beforeEach(() => { application = createApplicationBase() diff --git a/libs/application/templates/parental-leave/src/lib/parentalLeaveUtils.ts b/libs/application/templates/parental-leave/src/lib/parentalLeaveUtils.ts index e0a1ba0dffc1..321329d958a5 100644 --- a/libs/application/templates/parental-leave/src/lib/parentalLeaveUtils.ts +++ b/libs/application/templates/parental-leave/src/lib/parentalLeaveUtils.ts @@ -82,8 +82,9 @@ import { } from '../types' import { currentDateStartTime } from './parentalLeaveTemplateUtils' -export const getExpectedDateOfBirthOrAdoptionDate = ( +export const getExpectedDateOfBirthOrAdoptionDateOrBirthDate = ( application: Application, + returnBirthDate = false, ): string | undefined => { const selectedChild = getSelectedChild( application.answers, @@ -94,6 +95,12 @@ export const getExpectedDateOfBirthOrAdoptionDate = ( return undefined } + if (returnBirthDate) { + const { dateOfBirth } = getApplicationExternalData(application.externalData) + + if (dateOfBirth?.data?.dateOfBirth) return dateOfBirth?.data?.dateOfBirth + } + if (selectedChild.expectedDateOfBirth === '') return selectedChild.adoptionDate @@ -1302,8 +1309,8 @@ export const getLastValidPeriodEndDate = ( } export const getMinimumStartDate = (application: Application): Date => { - const expectedDateOfBirthOrAdoptionDate = - getExpectedDateOfBirthOrAdoptionDate(application) + const expectedDateOfBirthOrAdoptionDateOrBirthDate = + getExpectedDateOfBirthOrAdoptionDateOrBirthDate(application, true) const lastPeriodEndDate = getLastValidPeriodEndDate(application) const { applicationFundId } = getApplicationExternalData( application.externalData, @@ -1312,15 +1319,15 @@ export const getMinimumStartDate = (application: Application): Date => { const today = new Date() if (lastPeriodEndDate) { return lastPeriodEndDate - } else if (expectedDateOfBirthOrAdoptionDate) { - const expectedDateOfBirthOrAdoptionDateDate = new Date( - expectedDateOfBirthOrAdoptionDate, + } else if (expectedDateOfBirthOrAdoptionDateOrBirthDate) { + const expectedDateOfBirthOrAdoptionDateOrBirthDateDate = new Date( + expectedDateOfBirthOrAdoptionDateOrBirthDate, ) if (isParentalGrant(application)) { const beginningOfMonthOfExpectedDateOfBirth = addDays( - expectedDateOfBirthOrAdoptionDateDate, - expectedDateOfBirthOrAdoptionDateDate.getDate() * -1 + 1, + expectedDateOfBirthOrAdoptionDateOrBirthDateDate, + expectedDateOfBirthOrAdoptionDateOrBirthDateDate.getDate() * -1 + 1, ) return beginningOfMonthOfExpectedDateOfBirth } @@ -1328,7 +1335,7 @@ export const getMinimumStartDate = (application: Application): Date => { const beginningOfMonth = getBeginningOfThisMonth() const beginningOfMonth3MonthsAgo = getBeginningOfMonth3MonthsAgo() const leastStartDate = addMonths( - expectedDateOfBirthOrAdoptionDateDate, + expectedDateOfBirthOrAdoptionDateOrBirthDateDate, -minimumPeriodStartBeforeExpectedDateOfBirth, ) diff --git a/libs/application/ui-fields/src/lib/FindVehicleFormField/FindVehicleFormField.tsx b/libs/application/ui-fields/src/lib/FindVehicleFormField/FindVehicleFormField.tsx index 88d95be41086..3430ceaa64f3 100644 --- a/libs/application/ui-fields/src/lib/FindVehicleFormField/FindVehicleFormField.tsx +++ b/libs/application/ui-fields/src/lib/FindVehicleFormField/FindVehicleFormField.tsx @@ -104,6 +104,8 @@ const extractDetails = function ( ) { return { ...extractCommonVehicleInfo(response.basicVehicleInformation), + isDebtLess: true, + validationErrorMessages: response?.validationErrorMessages ?? [], } } else if ( isVehicleType( diff --git a/libs/auth-api-lib/src/index.ts b/libs/auth-api-lib/src/index.ts index 2e3de612b92e..6360687933cd 100644 --- a/libs/auth-api-lib/src/index.ts +++ b/libs/auth-api-lib/src/index.ts @@ -69,6 +69,7 @@ export * from './lib/resources/models/api-resource-secret.model' export * from './lib/resources/models/api-resource-user-claim.model' export * from './lib/resources/models/api-scope.model' export * from './lib/resources/models/api-scope-user-claim.model' +export * from './lib/resources/models/api-scope-delegation-type.model' export * from './lib/resources/models/api-scope-group.model' export * from './lib/resources/models/api-scope-user-access.model' export * from './lib/resources/models/api-scope-user.model' diff --git a/libs/auth-api-lib/src/lib/clients/clients.module.ts b/libs/auth-api-lib/src/lib/clients/clients.module.ts index 98ccce2b13aa..79d2b674b5eb 100644 --- a/libs/auth-api-lib/src/lib/clients/clients.module.ts +++ b/libs/auth-api-lib/src/lib/clients/clients.module.ts @@ -22,6 +22,7 @@ import { AdminTranslationService } from '../resources/admin/services/admin-trans import { ClientDelegationType } from './models/client-delegation-type.model' import { DelegationTypeModel } from '../delegations/models/delegation-type.model' import { DelegationProviderModel } from '../delegations/models/delegation-provider.model' +import { ApiScopeDelegationType } from '../resources/models/api-scope-delegation-type.model' @Module({ imports: [ @@ -41,6 +42,7 @@ import { DelegationProviderModel } from '../delegations/models/delegation-provid Domain, ApiScope, ApiScopeUserClaim, + ApiScopeDelegationType, ]), TranslationModule, ], diff --git a/libs/auth-api-lib/src/lib/delegations/models/delegation-type.model.ts b/libs/auth-api-lib/src/lib/delegations/models/delegation-type.model.ts index f9013d82e7f2..51527569ba3b 100644 --- a/libs/auth-api-lib/src/lib/delegations/models/delegation-type.model.ts +++ b/libs/auth-api-lib/src/lib/delegations/models/delegation-type.model.ts @@ -22,6 +22,8 @@ import { PersonalRepresentativeDelegationTypeModel } from '../../personal-repres import { DelegationTypeDto } from '../dto/delegation-type.dto' import { ClientDelegationType } from '../../clients/models/client-delegation-type.model' import { Client } from '../../clients/models/client.model' +import { ApiScopeDelegationType } from '../../resources/models/api-scope-delegation-type.model' +import { ApiScope } from '../../resources/models/api-scope.model' @Table({ tableName: 'delegation_type', @@ -65,6 +67,9 @@ export class DelegationTypeModel extends Model< @BelongsToMany(() => Client, () => ClientDelegationType) clients!: CreationOptional + @BelongsToMany(() => ApiScope, () => ApiScopeDelegationType) + apiScopes!: CreationOptional + @CreatedAt readonly created!: CreationOptional diff --git a/libs/auth-api-lib/src/lib/resources/admin/admin-scope.service.ts b/libs/auth-api-lib/src/lib/resources/admin/admin-scope.service.ts index a4fa03d845ff..94ca6aa64e81 100644 --- a/libs/auth-api-lib/src/lib/resources/admin/admin-scope.service.ts +++ b/libs/auth-api-lib/src/lib/resources/admin/admin-scope.service.ts @@ -5,14 +5,13 @@ import { Injectable, } from '@nestjs/common' import { InjectModel } from '@nestjs/sequelize' -import { Transaction } from 'sequelize' +import { Op, Transaction } from 'sequelize' import omit from 'lodash/omit' import { validatePermissionId } from '@island.is/auth/shared' import { isDefined } from '@island.is/shared/utils' import { ApiScope } from '../models/api-scope.model' -import { Client } from '../../clients/models/client.model' import { AdminCreateScopeDto } from './dto/admin-create-scope.dto' import { ApiScopeUserClaim } from '../models/api-scope-user-claim.model' import { AdminScopeDTO } from './dto/admin-scope.dto' @@ -26,6 +25,9 @@ import { TranslatedValueDto } from '../../translation/dto/translated-value.dto' import { TranslationService } from '../../translation/translation.service' import { User } from '@island.is/auth-nest-tools' import { AdminPortalScope } from '@island.is/auth/scopes' +import { AuthDelegationProvider, AuthDelegationType } from 'delegation' +import { ApiScopeDelegationType } from '../models/api-scope-delegation-type.model' +import { DelegationTypeModel } from '../../delegations/models/delegation-type.model' /** * This is a service that is used to access the admin scopes @@ -35,10 +37,12 @@ export class AdminScopeService { constructor( @InjectModel(ApiScope) private readonly apiScope: typeof ApiScope, - @InjectModel(Client) - private readonly clientModel: typeof Client, @InjectModel(ApiScopeUserClaim) private readonly apiScopeUserClaim: typeof ApiScopeUserClaim, + @InjectModel(ApiScopeDelegationType) + private readonly apiScopeDelegationType: typeof ApiScopeDelegationType, + @InjectModel(DelegationTypeModel) + private readonly delegationTypeModel: typeof DelegationTypeModel, private readonly adminTranslationService: AdminTranslationService, private readonly translationService: TranslationService, private sequelize: Sequelize, @@ -50,6 +54,9 @@ export class AdminScopeService { domainName: tenantId, enabled: true, }, + include: [ + { model: ApiScopeDelegationType, as: 'supportedDelegationTypes' }, + ], }) const translations = @@ -84,6 +91,9 @@ export class AdminScopeService { domainName: tenantId, enabled: true, }, + include: [ + { model: ApiScopeDelegationType, as: 'supportedDelegationTypes' }, + ], }) if (!apiScope) { @@ -149,10 +159,10 @@ export class AdminScopeService { throw new BadRequestException(translatedValuesErrorMsg) } - const apiScope = await this.sequelize.transaction(async (transaction) => { + await this.sequelize.transaction(async (transaction) => { const scope = await this.apiScope.create( { - ...input, + ...omit(input, ['displayName', 'description']), displayName, description, domainName: tenantId, @@ -177,9 +187,30 @@ export class AdminScopeService { transaction, ) + await this.addScopeDelegationTypes({ + apiScopeName: scope.name, + delegationBooleanTypes: input, + delegationTypes: input.supportedDelegationTypes, + transaction, + }) + return scope }) + const apiScope = await this.apiScope.findOne({ + where: { + name: input.name, + domainName: tenantId, + }, + include: [ + { model: ApiScopeDelegationType, as: 'supportedDelegationTypes' }, + ], + }) + + if (!apiScope) { + throw new Error('Failed to create scope') + } + const translations = await this.adminTranslationService.getApiScopeTranslations([ apiScope.name, @@ -302,7 +333,14 @@ export class AdminScopeService { // Update apiScope row and get the Icelandic translations for displayName and description await this.apiScope.update( { - ...omit(input, ['displayName', 'description']), + ...omit(input, [ + 'displayName', + 'description', + 'grantToProcuringHolders', + 'grantToLegalGuardians', + 'grantToPersonalRepresentatives', + 'allowExplicitDelegationGrant', + ]), ...(displayName && { displayName }), ...(description && { description }), }, @@ -314,6 +352,18 @@ export class AdminScopeService { }, ) + await this.addScopeDelegationTypes({ + apiScopeName: scopeName, + delegationBooleanTypes: input, + delegationTypes: input.addedDelegationTypes, + transaction, + }) + await this.removeScopeDelegationTypes({ + apiScopeName: scopeName, + delegationBooleanTypes: input, + delegationTypes: input.removedDelegationTypes, + transaction, + }) await this.updateScopeTranslatedValueFields(scopeName, input, transaction) }) @@ -341,4 +391,191 @@ export class AdminScopeService { // If there is a superUser field in the updated fields, the user must be a superUser return superUserUpdatedFields.length > 0 && isSuperUser } + + private async addScopeDelegationTypes({ + apiScopeName, + delegationBooleanTypes, + delegationTypes, + transaction, + }: { + apiScopeName: string + delegationTypes?: string[] + delegationBooleanTypes: { + allowExplicitDelegationGrant?: boolean + grantToLegalGuardians?: boolean + grantToProcuringHolders?: boolean + grantToPersonalRepresentatives?: boolean + } + transaction: Transaction + }) { + // boolean fields + const grantToProcuringHolders = + delegationTypes?.includes(AuthDelegationType.ProcurationHolder) || + delegationBooleanTypes.grantToProcuringHolders + const grantToLegalGuardians = + delegationTypes?.includes(AuthDelegationType.LegalGuardian) || + delegationBooleanTypes.grantToLegalGuardians + const grantToPersonalRepresentatives = + delegationTypes?.some((delegationType) => + delegationType.startsWith(AuthDelegationType.PersonalRepresentative), + ) || delegationBooleanTypes.grantToPersonalRepresentatives + const allowExplicitDelegationGrant = + delegationTypes?.includes(AuthDelegationType.Custom) || + delegationBooleanTypes.allowExplicitDelegationGrant + + // delegation types to add to api_scope_delegation_types table + const delegationTypesToAdd: string[] = [ + ...(allowExplicitDelegationGrant ? [AuthDelegationType.Custom] : []), + ...(grantToLegalGuardians ? [AuthDelegationType.LegalGuardian] : []), + ...(grantToProcuringHolders + ? [AuthDelegationType.ProcurationHolder] + : []), + ] + + if (grantToPersonalRepresentatives) { + const personalRepresentativeDelegationTypes = + await this.delegationTypeModel.findAll({ + where: { + provider: AuthDelegationProvider.PersonalRepresentativeRegistry, + }, + }) + + delegationTypesToAdd.push( + ...personalRepresentativeDelegationTypes.map( + (delegationType) => delegationType.id, + ), + ) + } + + // create delegation type rows + if (delegationTypesToAdd.length > 0) { + await Promise.all( + delegationTypesToAdd.map((delegationType) => + this.apiScopeDelegationType.upsert( + { + apiScopeName, + delegationType, + }, + { transaction }, + ), + ), + ) + } + + // update boolean fields + if ( + grantToLegalGuardians || + grantToPersonalRepresentatives || + grantToProcuringHolders || + allowExplicitDelegationGrant + ) { + await this.apiScope.update( + { + grantToLegalGuardians, + grantToPersonalRepresentatives, + grantToProcuringHolders, + allowExplicitDelegationGrant, + }, + { + transaction, + where: { + name: apiScopeName, + }, + }, + ) + } + } + + private async removeScopeDelegationTypes({ + apiScopeName, + delegationBooleanTypes, + delegationTypes, + transaction, + }: { + apiScopeName: string + delegationTypes?: string[] + delegationBooleanTypes: { + allowExplicitDelegationGrant?: boolean + grantToLegalGuardians?: boolean + grantToProcuringHolders?: boolean + grantToPersonalRepresentatives?: boolean + } + transaction: Transaction + }) { + // boolean fields + const grantToProcuringHolders = delegationTypes?.includes( + AuthDelegationType.ProcurationHolder, + ) + ? false + : delegationBooleanTypes.grantToProcuringHolders + const grantToLegalGuardians = delegationTypes?.includes( + AuthDelegationType.LegalGuardian, + ) + ? false + : delegationBooleanTypes.grantToLegalGuardians + const grantToPersonalRepresentatives = delegationTypes?.some( + (delegationType) => + delegationType.startsWith(AuthDelegationType.PersonalRepresentative), + ) + ? false + : delegationBooleanTypes.grantToPersonalRepresentatives + const allowExplicitDelegationGrant = delegationTypes?.includes( + AuthDelegationType.Custom, + ) + ? false + : delegationBooleanTypes.allowExplicitDelegationGrant + + // delegation types to remove from api_scope_delegation_types table + const delegationTypesToRemove = [ + ...(allowExplicitDelegationGrant === false + ? [AuthDelegationType.Custom] + : []), + ...(grantToLegalGuardians === false + ? [AuthDelegationType.LegalGuardian] + : []), + ...(grantToProcuringHolders === false + ? [AuthDelegationType.ProcurationHolder] + : []), + ...(grantToPersonalRepresentatives === false + ? [AuthDelegationType.PersonalRepresentative] + : []), + ] + + // remove delegation type rows + + await Promise.all( + delegationTypesToRemove.map((delegationType) => + this.apiScopeDelegationType.destroy({ + transaction, + where: { + apiScopeName, + delegationType: { [Op.startsWith]: delegationType }, + }, + }), + ), + ) + + // update boolean fields + if ( + grantToLegalGuardians === false || + grantToPersonalRepresentatives === false || + grantToProcuringHolders === false || + allowExplicitDelegationGrant === false + ) { + await this.apiScope.update( + { + grantToLegalGuardians, + grantToPersonalRepresentatives, + grantToProcuringHolders, + allowExplicitDelegationGrant, + }, + { + transaction, + where: { + name: apiScopeName, + }, + }, + ) + } + } } diff --git a/libs/auth-api-lib/src/lib/resources/admin/dto/admin-create-scope.dto.ts b/libs/auth-api-lib/src/lib/resources/admin/dto/admin-create-scope.dto.ts index fec53e21ec81..fbc5f678fdb8 100644 --- a/libs/auth-api-lib/src/lib/resources/admin/dto/admin-create-scope.dto.ts +++ b/libs/auth-api-lib/src/lib/resources/admin/dto/admin-create-scope.dto.ts @@ -1,11 +1,22 @@ -import { ApiProperty } from '@nestjs/swagger' -import { IsNotEmpty, IsString } from 'class-validator' +import { ApiProperty, ApiPropertyOptional, OmitType } from '@nestjs/swagger' +import { IsArray, IsNotEmpty, IsOptional, IsString } from 'class-validator' import { AdminPatchScopeDto } from './admin-patch-scope.dto' -export class AdminCreateScopeDto extends AdminPatchScopeDto { +export class AdminCreateScopeDto extends OmitType(AdminPatchScopeDto, [ + 'removedDelegationTypes', + 'addedDelegationTypes', +]) { @IsString() @IsNotEmpty() @ApiProperty({ example: '@island.is' }) name!: string + + @IsArray() + @IsOptional() + @ApiPropertyOptional({ + type: [String], + example: ['Custom'], + }) + supportedDelegationTypes?: string[] } diff --git a/libs/auth-api-lib/src/lib/resources/admin/dto/admin-patch-scope.dto.ts b/libs/auth-api-lib/src/lib/resources/admin/dto/admin-patch-scope.dto.ts index a55b5b46b6a0..e7873e7084a9 100644 --- a/libs/auth-api-lib/src/lib/resources/admin/dto/admin-patch-scope.dto.ts +++ b/libs/auth-api-lib/src/lib/resources/admin/dto/admin-patch-scope.dto.ts @@ -84,6 +84,22 @@ export class AdminPatchScopeDto { example: false, }) grantToPersonalRepresentatives?: boolean + + @IsArray() + @IsOptional() + @ApiPropertyOptional({ + type: [String], + example: ['Custom'], + }) + addedDelegationTypes?: string[] + + @IsArray() + @IsOptional() + @ApiPropertyOptional({ + type: [String], + example: ['Custom'], + }) + removedDelegationTypes?: string[] } export const superUserScopeFields = [ diff --git a/libs/auth-api-lib/src/lib/resources/dto/base/api-scope-base.dto.ts b/libs/auth-api-lib/src/lib/resources/dto/base/api-scope-base.dto.ts index 36adfc2e643f..c46dfdd256a7 100644 --- a/libs/auth-api-lib/src/lib/resources/dto/base/api-scope-base.dto.ts +++ b/libs/auth-api-lib/src/lib/resources/dto/base/api-scope-base.dto.ts @@ -6,8 +6,9 @@ import { IsInt, Min, Max, + IsArray, } from 'class-validator' -import { ApiProperty } from '@nestjs/swagger' +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger' export class ApiScopeBaseDTO { @IsBoolean() @@ -107,6 +108,13 @@ export class ApiScopeBaseDTO { }) readonly automaticDelegationGrant!: boolean + @IsArray() + @ApiPropertyOptional({ + type: [String], + example: ['Custom'], + }) + supportedDelegationTypes?: string[] + @IsBoolean() @IsNotEmpty() @ApiProperty({ diff --git a/libs/auth-api-lib/src/lib/resources/models/api-scope-delegation-type.model.ts b/libs/auth-api-lib/src/lib/resources/models/api-scope-delegation-type.model.ts index 77ece38fe869..6b3de98bd8ca 100644 --- a/libs/auth-api-lib/src/lib/resources/models/api-scope-delegation-type.model.ts +++ b/libs/auth-api-lib/src/lib/resources/models/api-scope-delegation-type.model.ts @@ -24,7 +24,7 @@ export class ApiScopeDelegationType extends Model { }) @ForeignKey(() => ApiScope) @ApiProperty() - api_scope_name!: string + apiScopeName!: string @PrimaryKey @Column({ @@ -33,7 +33,7 @@ export class ApiScopeDelegationType extends Model { }) @ForeignKey(() => DelegationTypeModel) @ApiProperty() - delegation_type!: string + delegationType!: string @CreatedAt @ApiProperty() diff --git a/libs/auth-api-lib/src/lib/resources/models/api-scope.model.ts b/libs/auth-api-lib/src/lib/resources/models/api-scope.model.ts index 8b476db0417e..4ecc2ef2be44 100644 --- a/libs/auth-api-lib/src/lib/resources/models/api-scope.model.ts +++ b/libs/auth-api-lib/src/lib/resources/models/api-scope.model.ts @@ -2,6 +2,7 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger' import { Optional } from 'sequelize' import { BelongsTo, + BelongsToMany, Column, CreatedAt, DataType, @@ -20,6 +21,8 @@ import { ApiScopeGroup } from './api-scope-group.model' import { ApiScopeUserAccess } from './api-scope-user-access.model' import { ApiScopeUserClaim } from './api-scope-user-claim.model' import { Domain } from './domain.model' +import { ApiScopeDelegationType } from './api-scope-delegation-type.model' +import { DelegationTypeModel } from '../../delegations/models/delegation-type.model' interface ModelAttributes { name: string @@ -254,10 +257,18 @@ export class ApiScope extends Model { @HasMany(() => ApiScopeUserAccess) apiScopeUserAccesses?: ApiScopeUserAccess[] + @HasMany(() => ApiScopeDelegationType) + supportedDelegationTypes?: ApiScopeDelegationType[] + @BelongsTo(() => Domain) @ApiPropertyOptional({ type: () => Domain }) domain?: Domain + @BelongsToMany(() => DelegationTypeModel, () => ApiScopeDelegationType) + delegationTypes?: Array< + DelegationTypeModel & { ApiScopeDelegationType: ApiScopeDelegationType } + > + toDTO(): ApiScopeDTO { return { name: this.name, @@ -277,6 +288,10 @@ export class ApiScope extends Model { emphasize: this.emphasize, domainName: this.domainName, isAccessControlled: this.isAccessControlled ?? undefined, + supportedDelegationTypes: + this.supportedDelegationTypes?.map( + ({ delegationType }) => delegationType, + ) ?? [], } } } diff --git a/libs/cache/src/lib/cache.ts b/libs/cache/src/lib/cache.ts index 65309a175a81..f6eda60fd8a8 100644 --- a/libs/cache/src/lib/cache.ts +++ b/libs/cache/src/lib/cache.ts @@ -4,6 +4,7 @@ import { caching } from 'cache-manager' import type { Config } from 'cache-manager' import { redisInsStore } from 'cache-manager-ioredis-yet' import { Cluster, ClusterNode, RedisOptions, ClusterOptions } from 'ioredis' +import { DEFAULT_CLUSTER_OPTIONS } from 'ioredis/built/cluster/ClusterOptions' import { logger } from '@island.is/logging' import Keyv from 'keyv' @@ -72,6 +73,22 @@ const parseNodes = (nodes: string[]): ClusterNode[] => } }) +const getEnvValueToNumber = ( + key: string, + defaultValue: T = undefined as unknown as T, +): number | typeof defaultValue => { + const envValue = process.env[key] + if (envValue) { + const numberValue = parseInt(envValue, 10) + if (Number.isNaN(numberValue)) { + logger.error(`Failed parsing key ${key} with value ${envValue}`) + return defaultValue + } + return numberValue + } + return defaultValue +} + const getRedisClusterOptions = ( options: Options, ): RedisOptions | ClusterOptions => { @@ -81,11 +98,17 @@ const getRedisClusterOptions = ( } return { keyPrefix: options.noPrefix ? undefined : `${options.name}:`, - slotsRefreshTimeout: 3000, - slotsRefreshInterval: 10000, + slotsRefreshTimeout: getEnvValueToNumber( + 'REDIS_SLOTS_REFRESH_TIMEOUT', + 10000, + ), + slotsRefreshInterval: getEnvValueToNumber( + 'REDIS_SLOTS_REFRESH_INTERVAL', + 15000, + ), connectTimeout: 5000, // https://www.npmjs.com/package/ioredis#special-note-aws-elasticache-clusters-with-tls - dnsLookup: (address, callback) => callback(null, address, 0), + dnsLookup: (address, callback) => callback(null, address), redisOptions, reconnectOnError: (err) => { logger.error(`Reconnect on error: ${err}`) diff --git a/libs/clients/administration-of-occupational-safety-and-health/src/lib/api.provider.ts b/libs/clients/administration-of-occupational-safety-and-health/src/lib/api.provider.ts index 814856d933ff..cb395bd42b25 100644 --- a/libs/clients/administration-of-occupational-safety-and-health/src/lib/api.provider.ts +++ b/libs/clients/administration-of-occupational-safety-and-health/src/lib/api.provider.ts @@ -10,7 +10,6 @@ export const ApiConfig = { fetchApi: createEnhancedFetch({ name: 'clients-administration-of-occupational-safety-and-health', logErrorResponseBody: true, - treat400ResponsesAsErrors: true, organizationSlug: 'vinnueftirlitid', }), basePath: 'https://ws.ver.is/namskeid', diff --git a/libs/clients/aircraft-registry/src/lib/apiConfig.ts b/libs/clients/aircraft-registry/src/lib/apiConfig.ts index 1ee151e9a9bf..9ee35ccf05f4 100644 --- a/libs/clients/aircraft-registry/src/lib/apiConfig.ts +++ b/libs/clients/aircraft-registry/src/lib/apiConfig.ts @@ -18,7 +18,6 @@ export const ApiConfig = { fetchApi: createEnhancedFetch({ name: 'clients-aircraft-registry', organizationSlug: 'samgongustofa', - treat400ResponsesAsErrors: true, }), basePath: `${xroadConfig.xRoadBasePath}/r1/${config.xRoadServicePath}`, headers: { diff --git a/libs/clients/housing-benefit-calculator/src/lib/housing-benefit-calculator.provider.ts b/libs/clients/housing-benefit-calculator/src/lib/housing-benefit-calculator.provider.ts index e308e6e7378b..f57b0487fb7f 100644 --- a/libs/clients/housing-benefit-calculator/src/lib/housing-benefit-calculator.provider.ts +++ b/libs/clients/housing-benefit-calculator/src/lib/housing-benefit-calculator.provider.ts @@ -18,7 +18,6 @@ export const ApiConfig = { fetchApi: createEnhancedFetch({ name: 'clients-housing-benefit-calculator', logErrorResponseBody: true, - treat400ResponsesAsErrors: true, }), basePath: `${xroadConfig.xRoadBasePath}/r1/${config.xRoadServicePath}`, headers: { diff --git a/libs/clients/icelandic-government-institution-vacancies/src/lib/apiConfig.ts b/libs/clients/icelandic-government-institution-vacancies/src/lib/apiConfig.ts index 6923ecb5fe3a..e7164861cb18 100644 --- a/libs/clients/icelandic-government-institution-vacancies/src/lib/apiConfig.ts +++ b/libs/clients/icelandic-government-institution-vacancies/src/lib/apiConfig.ts @@ -26,7 +26,6 @@ export const ApiConfig = { name: 'clients-icelandic-government-institution-vacancies', organizationSlug: 'fjarsysla-rikisins', logErrorResponseBody: true, - treat400ResponsesAsErrors: true, timeout: 20000, }), basePath: `${xroadConfig.xRoadBasePath}/r1/${config.xRoadServicePath}`, diff --git a/libs/clients/icelandic-health-insurance/health-insurance/src/lib/clients-health-insurance-v2.module.ts b/libs/clients/icelandic-health-insurance/health-insurance/src/lib/clients-health-insurance-v2.module.ts index 3e6282372e69..828548a611da 100644 --- a/libs/clients/icelandic-health-insurance/health-insurance/src/lib/clients-health-insurance-v2.module.ts +++ b/libs/clients/icelandic-health-insurance/health-insurance/src/lib/clients-health-insurance-v2.module.ts @@ -13,7 +13,6 @@ export class HealthInsuranceV2Client { fetchApi: createEnhancedFetch({ name: 'clients-health-insurance', organizationSlug: 'sjukratryggingar', - treat400ResponsesAsErrors: true, logErrorResponseBody: true, timeout: 20000, // needed because the external service is taking a while to respond to submitting the document }), diff --git a/libs/clients/middlewares/README.md b/libs/clients/middlewares/README.md index 829555ea42de..b6e12bdae696 100644 --- a/libs/clients/middlewares/README.md +++ b/libs/clients/middlewares/README.md @@ -26,7 +26,6 @@ A new library providing an createEnhancedFetch function. - `name: string` - Name of fetch function. Used in logs and opossum stats. - `enableCircuitBreaker?: boolean` - Should use circuit breaker for requests. Defaults to `true`. - `timeout?: number | false` - Timeout for requests. Logged and thrown as errors. May cause circuit breaker to open. Defaults to `10000`ms. Can be disabled by passing false. -- `treat400ResponsesAsErrors?: boolean` - If `true`, then too many 400 responses may cause the circuit to open. Either way these responses will be logged and thrown. Defaults to `false`. - `logErrorResponseBody?: boolean` - If `true`, then non-200 response bodies will be consumed and included in the error object and logged as `body`. - `keepAlive?: boolean | number` - Configures keepAlive for requests. If `false`, never reuse connections. If `true`, reuse connection with a maximum idle timeout of 10 seconds. By passing a number you can override the idle connection timeout. Defaults to `true`. - `clientCertificate?: ClientCertificateOptions` - Configures client certificate for requests. diff --git a/libs/clients/middlewares/src/lib/createEnhancedFetch.spec.ts b/libs/clients/middlewares/src/lib/createEnhancedFetch.spec.ts index fa3892ec4a8f..9cf20b826136 100644 --- a/libs/clients/middlewares/src/lib/createEnhancedFetch.spec.ts +++ b/libs/clients/middlewares/src/lib/createEnhancedFetch.spec.ts @@ -206,20 +206,4 @@ describe('EnhancedFetch', () => { ) expect(env.fetch).toHaveBeenCalledTimes(2) }) - - it('can be configured to open circuit for 400 errors', async () => { - // Arrange - env = setupTestEnv({ treat400ResponsesAsErrors: true }) - env.fetch.mockResolvedValue(fakeResponse('Error', { status: 400 })) - await env.enhancedFetch(testUrl).catch(() => null) - - // Act - const promise = env.enhancedFetch(testUrl) - - // Assert - await expect(promise).rejects.toThrowErrorMatchingInlineSnapshot( - `"Breaker is open"`, - ) - expect(env.fetch).toHaveBeenCalledTimes(1) - }) }) diff --git a/libs/clients/middlewares/src/lib/createEnhancedFetch.ts b/libs/clients/middlewares/src/lib/createEnhancedFetch.ts index 5b3f0fafbf6d..5877ad214d84 100644 --- a/libs/clients/middlewares/src/lib/createEnhancedFetch.ts +++ b/libs/clients/middlewares/src/lib/createEnhancedFetch.ts @@ -63,12 +63,6 @@ export interface EnhancedFetchOptions { */ forwardAuthUserAgent?: boolean - /** - * By default, 400 responses are considered warnings and will not open the circuit. - * Either way they will be logged and thrown. - */ - treat400ResponsesAsErrors?: boolean - /** * If true (default), Enhanced Fetch will log error response bodies. * Should be set to false if error objects may have sensitive information or PII. @@ -159,7 +153,6 @@ export const createEnhancedFetch = ( metricsClient = new DogStatsD({ prefix: `${options.name}.` }), organizationSlug, } = options - const treat400ResponsesAsErrors = options.treat400ResponsesAsErrors === true const freeSocketTimeout = typeof keepAlive === 'number' ? keepAlive @@ -207,7 +200,6 @@ export const createEnhancedFetch = ( builder.wrap(withCircuitBreaker, { name, logger, - treat400ResponsesAsErrors, opossum, }) } @@ -233,7 +225,6 @@ export const createEnhancedFetch = ( builder.wrap(withErrorLog, { name, logger, - treat400ResponsesAsErrors, }) return builder.getFetch() diff --git a/libs/clients/middlewares/src/lib/withCircuitBreaker.ts b/libs/clients/middlewares/src/lib/withCircuitBreaker.ts index d0cb6a2aa654..4e8cd1dd86af 100644 --- a/libs/clients/middlewares/src/lib/withCircuitBreaker.ts +++ b/libs/clients/middlewares/src/lib/withCircuitBreaker.ts @@ -5,7 +5,6 @@ import { MiddlewareAPI } from './nodeFetch' import { FetchError } from './FetchError' export interface CircuitBreakerOptions { - treat400ResponsesAsErrors: boolean opossum: CircuitBreaker.Options fetch: MiddlewareAPI name: string @@ -13,20 +12,17 @@ export interface CircuitBreakerOptions { } export function withCircuitBreaker({ - treat400ResponsesAsErrors, opossum, fetch, name, logger, }: CircuitBreakerOptions): MiddlewareAPI { - const errorFilter = treat400ResponsesAsErrors - ? opossum?.errorFilter - : (error: FetchError) => { - if (error.name === 'FetchError' && error.status < 500) { - return true - } - return opossum?.errorFilter?.(error) ?? false - } + const errorFilter = (error: FetchError) => { + if (error.name === 'FetchError' && error.status < 500) { + return true + } + return opossum?.errorFilter?.(error) ?? false + } const breaker = new CircuitBreaker(fetch, { name, diff --git a/libs/clients/middlewares/src/lib/withErrorLog.ts b/libs/clients/middlewares/src/lib/withErrorLog.ts index 3993431cfde5..cec5c27ffe5d 100644 --- a/libs/clients/middlewares/src/lib/withErrorLog.ts +++ b/libs/clients/middlewares/src/lib/withErrorLog.ts @@ -6,23 +6,17 @@ import { FetchError } from './FetchError' interface ErrorLogOptions extends FetchMiddlewareOptions { name: string logger: Logger - treat400ResponsesAsErrors: boolean } export function withErrorLog({ name, fetch, - treat400ResponsesAsErrors, logger, }: ErrorLogOptions): MiddlewareAPI { - return (request) => { + return async (request) => { return fetch(request).catch((error: Error) => { const logLevel = - error instanceof FetchError && - error.status < 500 && - !treat400ResponsesAsErrors - ? 'warn' - : 'error' + error instanceof FetchError && error.status < 500 ? 'warn' : 'error' const cacheStatus = (error instanceof FetchError && error.response.headers.get('cache-status')) ?? diff --git a/libs/clients/ship-registry/src/lib/ship-registry.provider.ts b/libs/clients/ship-registry/src/lib/ship-registry.provider.ts index a9bb0e91df01..1dd654f28bb1 100644 --- a/libs/clients/ship-registry/src/lib/ship-registry.provider.ts +++ b/libs/clients/ship-registry/src/lib/ship-registry.provider.ts @@ -18,7 +18,6 @@ export const ApiConfig = { fetchApi: createEnhancedFetch({ name: 'clients-ship-registry', logErrorResponseBody: true, - treat400ResponsesAsErrors: true, }), basePath: `${xroadConfig.xRoadBasePath}/r1/${config.xRoadServicePath}`, headers: { diff --git a/libs/cms/src/lib/generated/contentfulTypes.d.ts b/libs/cms/src/lib/generated/contentfulTypes.d.ts index ebd76e4ecbcb..487a752c61e7 100644 --- a/libs/cms/src/lib/generated/contentfulTypes.d.ts +++ b/libs/cms/src/lib/generated/contentfulTypes.d.ts @@ -580,6 +580,9 @@ export interface IChartFields { /** Custom Style Config */ customStyleConfig?: Record | undefined + + /** Reduce and round value */ + reduceAndRoundValue?: boolean | undefined } /** A wrapper to render any graphical representation of data using [Chart Component]s. */ diff --git a/libs/cms/src/lib/models/chart.model.ts b/libs/cms/src/lib/models/chart.model.ts index a8af7e5a125f..9f67e4d2182d 100644 --- a/libs/cms/src/lib/models/chart.model.ts +++ b/libs/cms/src/lib/models/chart.model.ts @@ -56,6 +56,9 @@ export class Chart { @Field({ nullable: true }) customStyleConfig?: string + + @Field({ nullable: true }) + reduceAndRoundValue?: boolean } export const mapChart = ({ sys, fields }: IChart): SystemMetadata => { @@ -81,5 +84,6 @@ export const mapChart = ({ sys, fields }: IChart): SystemMetadata => { customStyleConfig: fields.customStyleConfig ? JSON.stringify(fields.customStyleConfig) : undefined, + reduceAndRoundValue: fields.reduceAndRoundValue ?? true, } } diff --git a/libs/cms/src/lib/models/organizationSubpage.model.ts b/libs/cms/src/lib/models/organizationSubpage.model.ts index c28ee54a264d..8a6d26cdcb79 100644 --- a/libs/cms/src/lib/models/organizationSubpage.model.ts +++ b/libs/cms/src/lib/models/organizationSubpage.model.ts @@ -68,7 +68,7 @@ export const mapOrganizationSubpage = ({ title: fields.title ?? '', shortTitle: fields.shortTitle || fields.title, slug: (fields.slug ?? '').trim(), - url: [fields.organizationPage?.fields?.slug, fields.slug], + url: [fields.organizationPage?.fields?.slug ?? '', fields.slug ?? ''], intro: fields.intro ?? '', description: fields.description && fields.sliceCustomRenderer !== 'SliceTableOfContents' diff --git a/libs/cms/src/lib/search/importers/organizationSubpage.service.ts b/libs/cms/src/lib/search/importers/organizationSubpage.service.ts index 07c8cdd74ec7..f533c8a0e20a 100644 --- a/libs/cms/src/lib/search/importers/organizationSubpage.service.ts +++ b/libs/cms/src/lib/search/importers/organizationSubpage.service.ts @@ -19,7 +19,9 @@ export class OrganizationSubpageSyncService return entries.filter( (entry: Entry): entry is IOrganizationSubpage => entry.sys.contentType.sys.id === 'organizationSubpage' && - !!entry.fields.title, + !!entry.fields.title && + !!entry.fields.slug && + !!entry.fields.organizationPage?.fields?.slug, ) } diff --git a/libs/dokobit-signing/src/lib/signing.config.ts b/libs/dokobit-signing/src/lib/signing.config.ts index a9d06766c939..46fb868f5f33 100644 --- a/libs/dokobit-signing/src/lib/signing.config.ts +++ b/libs/dokobit-signing/src/lib/signing.config.ts @@ -6,5 +6,11 @@ export const signingModuleConfig = defineConfig({ production: env.optional('NODE_ENV') === 'production', url: env.required('DOKOBIT_URL', 'https://developers.dokobit.com'), accessToken: env.required('DOKOBIT_ACCESS_TOKEN', ''), + pollDurationSeconds: +( + env.optional('DOKOBIT_POLL_DURATION_SECONDS') ?? '120' + ), + pollIntervalSeconds: +( + env.optional('DOKOBIT_POLL_INTERVAL_SECONDS') ?? '5' + ), }), }) diff --git a/libs/dokobit-signing/src/lib/signing.service.spec.ts b/libs/dokobit-signing/src/lib/signing.service.spec.ts index 9371b7473b1d..1f44cfaf0e78 100644 --- a/libs/dokobit-signing/src/lib/signing.service.spec.ts +++ b/libs/dokobit-signing/src/lib/signing.service.spec.ts @@ -39,7 +39,7 @@ const testStatusResponse = { }, } -jest.mock('form-data', function () { +jest.mock('form-data', () => { return function () { this.append = jest.fn(function (key: string, value: string) { this[key] = value @@ -48,29 +48,32 @@ jest.mock('form-data', function () { } }) -const fetchMock = jest.fn(function ( - url: RequestInfo, - // The init argument is needed for the mock to work - init?: RequestInit, // eslint-disable-line @typescript-eslint/no-unused-vars -) { - switch (url) { - case testSignUrl: - return { - json: async function () { - return testSignResponse - }, - } - case testStatusUrl: - return { - json: async function () { - return testStatusResponse - }, - } - default: - throw new Error() - } -}) -jest.mock('node-fetch', function () { +const fetchMock = jest.fn( + ( + url: RequestInfo, + // The init argument is needed for the mock to work + init?: RequestInit, // eslint-disable-line @typescript-eslint/no-unused-vars + ) => { + switch (url) { + case testSignUrl: + return { + json: async function () { + return testSignResponse + }, + } + case testStatusUrl: + return { + json: async function () { + return testStatusResponse + }, + } + default: + throw new Error() + } + }, +) + +jest.mock('node-fetch', () => { return async function (url: RequestInfo, init: RequestInit) { return fetchMock(url, init) } @@ -143,5 +146,5 @@ describe('SigningService', () => { // Verify sign status expect(fetchMock).toHaveBeenCalledWith(testStatusUrl, undefined) - }) + }, 5500) }) diff --git a/libs/dokobit-signing/src/lib/signing.service.ts b/libs/dokobit-signing/src/lib/signing.service.ts index bc0508d39433..fd627cb8becc 100644 --- a/libs/dokobit-signing/src/lib/signing.service.ts +++ b/libs/dokobit-signing/src/lib/signing.service.ts @@ -166,7 +166,14 @@ export class SigningService extends DataSource { // At the same time, the mobile user gets a long time to complete the signature // We need to try longer than the mobile signature timeout, but not too long // Later, we may decide to return after one call and let the caller handle retries - for (let i = 1; i < 120; i++) { + for ( + let i = 1; + i < this.config.pollDurationSeconds / this.config.pollIntervalSeconds; + i++ + ) { + // Wait a second + await this.delay(this.config.pollIntervalSeconds * 1000) + const res = await fetch( `${this.config.url}/mobile/sign/status/${documentToken}.json?access_token=${this.config.accessToken}`, ) @@ -184,9 +191,6 @@ export class SigningService extends DataSource { resStatus.message, ) } - - // Wait a second - await this.delay(1000) } throw new DokobitError( diff --git a/libs/financial-aid/shared/src/lib/interfaces.ts b/libs/financial-aid/shared/src/lib/interfaces.ts index f8959f11f11b..adbc31d3463c 100644 --- a/libs/financial-aid/shared/src/lib/interfaces.ts +++ b/libs/financial-aid/shared/src/lib/interfaces.ts @@ -342,6 +342,7 @@ export interface Application { homeCircumstancesCustom?: string studentCustom?: string formComment?: string + childrenComment?: string spouseFormComment?: string state: ApplicationState files?: ApplicationFile[] diff --git a/libs/island-ui/core/src/lib/Select/Select.stories.tsx b/libs/island-ui/core/src/lib/Select/Select.stories.tsx index 61f83ec39c7b..e38845851965 100644 --- a/libs/island-ui/core/src/lib/Select/Select.stories.tsx +++ b/libs/island-ui/core/src/lib/Select/Select.stories.tsx @@ -2,91 +2,141 @@ import React from 'react' import { withFigma } from '../../utils/withFigma' import { Select } from './Select' +import type { Meta, StoryObj } from '@storybook/react' -export default { +const config: Meta = { title: 'Form/Select', component: Select, parameters: withFigma('Select'), + argTypes: { + name: { description: 'Field name' }, + label: { description: 'Label text', control: { type: 'text' } }, + placeholder: { description: 'Placeholder text' }, + backgroundColor: { + description: 'Background color', + options: ['white', 'blue'], + control: { type: 'radio' }, + defaultValue: 'white', + }, + noOptionsMessage: { description: 'No options message' }, + size: { + description: 'Field size', + options: ['xs', 'sm', 'md'], + control: { type: 'radio' }, + }, + options: { description: 'Select options' }, + isDisabled: { description: 'Is select field disabled' }, + isClearable: { description: 'Is select field clearable' }, + isSearchable: { description: 'Is select field searchable' }, + required: { description: 'Is select field required' }, + hasError: { description: 'Does select field has error' }, + errorMessage: { + description: 'Error message description', + control: { type: 'text' }, + }, + icon: { description: 'Icon name' }, + }, } -const Template = (args) => + +) -const selectArgs = { - name: 'select', - label: 'Label text', +export const Default: SelectProps = Template.bind({}) +Default.args = { + name: 'Select', + label: 'Select label text', placeholder: 'Text', - options: options, + noOptionsMessage: 'No options', + options: [ + { + label: 'Text 1', + value: '0', + }, + { + label: 'Text 2', + value: '1', + }, + { + label: 'Text 3', + value: '2', + }, + ], backgroundColor: 'white', + size: 'md', isDisabled: false, - noOptionsMessage: 'Enginn valmöguleiki', isClearable: false, isSearchable: false, - size: 'md', hasError: false, + required: false, + errorMessage: undefined, + icon: undefined, } -export const Default = Template.bind({}) -Default.args = selectArgs - -export const Blue = Template.bind({}) -Blue.args = { - ...selectArgs, +export const BlueBackground = Template.bind({}) +BlueBackground.args = { + ...Default.args, backgroundColor: 'blue', } +export const NoOptions = Template.bind({}) +NoOptions.args = { + ...Default.args, + options: [], + noOptionsMessage: 'No options', +} + +export const SizeSm = Template.bind({}) +SizeSm.args = { + ...Default.args, + size: 'sm', +} + +export const SizeXs = Template.bind({}) +SizeXs.args = { + ...Default.args, + size: 'xs', +} + export const Disabled = Template.bind({}) Disabled.args = { - ...selectArgs, + ...Default.args, isDisabled: true, } -export const NoOption = Template.bind({}) -NoOption.args = { - ...selectArgs, - noOptionsMessage: 'Enginn valmöguleiki', -} - export const Clearable = Template.bind({}) Clearable.args = { - ...selectArgs, + ...Default.args, isClearable: true, } export const Searchable = Template.bind({}) Searchable.args = { - ...selectArgs, + ...Default.args, isSearchable: true, + placeholder: 'Type to search', } -export const SizeSm = Template.bind({}) -SizeSm.args = { - ...selectArgs, - size: 'sm', +export const WithDifferentIcon = Template.bind({}) +WithDifferentIcon.args = { + ...Default.args, + icon: 'ellipsisVertical', } -export const SizeXs = Template.bind({}) -SizeXs.args = { - ...selectArgs, - size: 'xs', +export const Required = Template.bind({}) +Required.args = { + ...Default.args, + required: true, } -export const WithError = Template.bind({}) -WithError.args = { - ...selectArgs, +export const HasError = Template.bind({}) +HasError.args = { + ...Default.args, hasError: true, + errorMessage: 'This is an error message', } diff --git a/libs/island-ui/core/src/lib/Select/Select.types.ts b/libs/island-ui/core/src/lib/Select/Select.types.ts index 6d4c1994dc6f..726508ae7037 100644 --- a/libs/island-ui/core/src/lib/Select/Select.types.ts +++ b/libs/island-ui/core/src/lib/Select/Select.types.ts @@ -18,6 +18,28 @@ export interface AriaError { 'aria-invalid': boolean 'aria-describedby': string } + +export type PropsBase = { + // Common custom props added for our custom Select + backgroundColor?: InputBackgroundColor + errorMessage?: string + filterConfig?: FilterConfig + hasError?: boolean + icon?: IconTypes + isCreatable?: boolean + label?: string + size?: 'xs' | 'sm' | 'md' + + // Added as prop to forward to custom Input component + ariaError?: AriaError + + // Added for CountryCodeSelect to forward prop to custom IndicatorsContainer component + inputHasLabel?: boolean + + // Added for test support + dataTestId?: string +} + // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore make web strict declare module 'react-select/dist/declarations/src/Select' { @@ -25,26 +47,7 @@ declare module 'react-select/dist/declarations/src/Select' { Option, IsMulti extends boolean, Group extends GroupBase