From 3e2136e78a8bd4705d90caa959e374786382d1ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAnar=20Vestmann?= <43557895+RunarVestmann@users.noreply.github.com> Date: Fri, 20 Sep 2024 08:56:42 +0000 Subject: [PATCH 01/18] fix(web): Pension Calculator - Limit how far into future start month can be (#16094) * Limit how far into future months can be * Add more robust method * Reuse variable --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../PensionCalculator.tsx | 124 ++++++++++++------ 1 file changed, 86 insertions(+), 38 deletions(-) diff --git a/apps/web/screens/Organization/SocialInsuranceAdministration/PensionCalculator.tsx b/apps/web/screens/Organization/SocialInsuranceAdministration/PensionCalculator.tsx index efc9ed065e6c..f72249630a5a 100644 --- a/apps/web/screens/Organization/SocialInsuranceAdministration/PensionCalculator.tsx +++ b/apps/web/screens/Organization/SocialInsuranceAdministration/PensionCalculator.tsx @@ -159,6 +159,11 @@ const PensionCalculator: CustomScreen = ({ ? 12 * 7 : 12 * 2 + const maxMonthPensionDelay = + typeof birthYear === 'number' && birthYear < 1952 + ? maxMonthPensionDelayIfBorn1951OrEarlier + : maxMonthPensionDelayIfBornAfter1951 + const basePensionTypeOptions = useMemo[]>(() => { const options = [ { @@ -390,11 +395,6 @@ const PensionCalculator: CustomScreen = ({ months: -maxMonthPensionHurry, }).getFullYear() - const maxMonthPensionDelay = - typeof birthYear === 'number' && birthYear < 1952 - ? maxMonthPensionDelayIfBorn1951OrEarlier - : maxMonthPensionDelayIfBornAfter1951 - const maxYear = add(defaultPensionDate, { months: maxMonthPensionDelay, }).getFullYear() @@ -408,13 +408,7 @@ const PensionCalculator: CustomScreen = ({ } return options - }, [ - birthYear, - defaultPensionDate, - maxMonthPensionDelayIfBorn1951OrEarlier, - maxMonthPensionDelayIfBornAfter1951, - maxMonthPensionHurry, - ]) + }, [defaultPensionDate, maxMonthPensionDelay, maxMonthPensionHurry]) const title = `${formatMessage(translationStrings.mainTitle)} ${ dateOfCalculationsOptions.find((o) => o.value === dateOfCalculations) @@ -422,22 +416,31 @@ const PensionCalculator: CustomScreen = ({ }` const startMonthOptions = useMemo(() => { - if ( - startYear === startYearOptions?.[0]?.value && - typeof birthMonth === 'number' && - typeof startMonth === 'number' - ) { - if (startMonth < birthMonth + 1) { - methods.setValue('startMonth', birthMonth + 1) - } - return monthOptions.filter(({ value }) => value >= birthMonth + 1) + if (!defaultPensionDate) { + return monthOptions } + + if (startYear === startYearOptions[0]?.value) { + const minMonth = add(defaultPensionDate, { + months: -maxMonthPensionHurry, + }).getMonth() + return monthOptions.filter((month) => month.value >= minMonth) + } + + if (startYear === startYearOptions[startYearOptions.length - 1]?.value) { + const maxMonth = add(defaultPensionDate, { + months: maxMonthPensionDelay, + }).getMonth() + + return monthOptions.filter((month) => month.value <= maxMonth) + } + return monthOptions }, [ - birthMonth, - methods, + defaultPensionDate, + maxMonthPensionDelay, + maxMonthPensionHurry, monthOptions, - startMonth, startYear, startYearOptions, ]) @@ -600,20 +603,16 @@ const PensionCalculator: CustomScreen = ({ translationStrings.birthMonthPlaceholder, )} onSelect={(option) => { - if (option.value > 10) { - methods.setValue('startMonth', 0) - if (startYear) { - methods.setValue( - 'startYear', - startYear + 1, - ) - } - } else { - methods.setValue( - 'startMonth', - option.value + 1, - ) - } + methods.setValue( + 'startMonth', + option.value > 10 ? 0 : option.value + 1, + ) + methods.setValue( + 'startYear', + birthYear + + defaultPensionAge + + (option.value > 10 ? 1 : 0), + ) }} /> @@ -694,6 +693,55 @@ const PensionCalculator: CustomScreen = ({ placeholder={formatMessage( translationStrings.startYearPlaceholder, )} + onSelect={(option) => { + if (!defaultPensionDate) { + return + } + if ( + option.value === + startYearOptions[0]?.value + ) { + const minMonth = add( + defaultPensionDate, + { + months: -maxMonthPensionHurry, + }, + ).getMonth() + if ( + typeof startMonth === 'number' && + startMonth < minMonth + ) { + methods.setValue( + 'startMonth', + minMonth, + ) + } + } + + if ( + option.value === + startYearOptions[ + startYearOptions.length - 1 + ]?.value + ) { + const maxMonth = add( + defaultPensionDate, + { + months: maxMonthPensionDelay, + }, + ).getMonth() + + if ( + typeof startMonth === 'number' && + startMonth > maxMonth + ) { + methods.setValue( + 'startMonth', + maxMonth, + ) + } + } + }} /> From 549f2888ad1fad031f1a496ce28c07fe56de827b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B3n=20Bjarni=20=C3=93lafsson?= <92530555+jonbjarnio@users.noreply.github.com> Date: Fri, 20 Sep 2024 09:15:03 +0000 Subject: [PATCH 02/18] feat(ojoi): Add auth to client endpoint (#16090) * Added auth to application endpoints * Removed console logs and updated logging level to appropriate one --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../src/lib/ojoiApplication.resolver.ts | 50 ++++--- .../src/lib/ojoiApplication.service.ts | 125 +++++++++++------- .../official-journal-of-iceland.service.ts | 9 +- .../src/lib/ojoiApplicationClient.config.ts | 4 +- .../src/lib/ojoiApplicationClient.provider.ts | 13 +- .../src/lib/ojoiApplicationClient.service.ts | 81 +++++++++--- 6 files changed, 187 insertions(+), 95 deletions(-) diff --git a/libs/api/domains/official-journal-of-iceland-application/src/lib/ojoiApplication.resolver.ts b/libs/api/domains/official-journal-of-iceland-application/src/lib/ojoiApplication.resolver.ts index 8d7c5cee47f7..9ea5f38765d8 100644 --- a/libs/api/domains/official-journal-of-iceland-application/src/lib/ojoiApplication.resolver.ts +++ b/libs/api/domains/official-journal-of-iceland-application/src/lib/ojoiApplication.resolver.ts @@ -1,5 +1,10 @@ import { Args, Mutation, Query, Resolver } from '@nestjs/graphql' -import { IdsUserGuard, Scopes, ScopesGuard } from '@island.is/auth-nest-tools' +import { + CurrentUser, + IdsUserGuard, + Scopes, + ScopesGuard, +} from '@island.is/auth-nest-tools' import { ApiScope } from '@island.is/auth/scopes' import { FeatureFlag, Features } from '@island.is/nest/feature-flags' import { OfficialJournalOfIcelandApplicationService } from './ojoiApplication.service' @@ -7,7 +12,6 @@ import { GetCommentsInput } from '../models/getComments.input' import { GetCommentsResponse } from '../models/getComments.response' import { PostCommentInput } from '../models/postComment.input' import { PostCommentResponse } from '../models/postComment.response' -import { PostApplicationInput } from '../models/postApplication.input' import { UseGuards } from '@nestjs/common' import { CaseGetPriceResponse } from '../models/getPrice.response' import { GetPdfUrlResponse } from '../models/getPdfUrlResponse' @@ -18,6 +22,7 @@ import { AddApplicationAttachmentInput } from '../models/addApplicationAttachmen import { GetApplicationAttachmentInput } from '../models/getApplicationAttachment.input' import { GetApplicationAttachmentsResponse } from '../models/getApplicationAttachments.response' import { DeleteApplicationAttachmentInput } from '../models/deleteApplicationAttachment.input' +import type { User } from '@island.is/auth-nest-tools' @Scopes(ApiScope.internal) @UseGuards(IdsUserGuard, ScopesGuard) @@ -31,36 +36,35 @@ export class OfficialJournalOfIcelandApplicationResolver { @Query(() => GetCommentsResponse, { name: 'officialJournalOfIcelandApplicationGetComments', }) - getComments(@Args('input') input: GetCommentsInput) { - return this.ojoiApplicationService.getComments(input) + getComments( + @Args('input') input: GetCommentsInput, + @CurrentUser() user: User, + ) { + return this.ojoiApplicationService.getComments(input, user) } @Mutation(() => PostCommentResponse, { name: 'officialJournalOfIcelandApplicationPostComment', }) - postComment(@Args('input') input: PostCommentInput) { - return this.ojoiApplicationService.postComment(input) - } - - @Query(() => Boolean, { - name: 'officialJournalOfIcelandApplicationPostApplication', - }) - postApplication(@Args('input') input: PostApplicationInput) { - return this.ojoiApplicationService.postApplication(input) + postComment( + @Args('input') input: PostCommentInput, + @CurrentUser() user: User, + ) { + return this.ojoiApplicationService.postComment(input, user) } @Query(() => CaseGetPriceResponse, { name: 'officialJournalOfIcelandApplicationGetPrice', }) - getPrice(@Args('id') id: string) { - return this.ojoiApplicationService.getPrice(id) + getPrice(@Args('id') id: string, @CurrentUser() user: User) { + return this.ojoiApplicationService.getPrice(id, user) } @Query(() => GetPdfUrlResponse, { name: 'officialJournalOfIcelandApplicationGetPdfUrl', }) - getPdfUrl(@Args('id') id: string) { - return this.ojoiApplicationService.getPdfUrl(id) + getPdfUrl(@Args('id') id: string, @CurrentUser() user: User) { + return this.ojoiApplicationService.getPdfUrl(id, user) } @Mutation(() => GetPresignedUrlResponse, { @@ -69,8 +73,9 @@ export class OfficialJournalOfIcelandApplicationResolver { getPresignedUrl( @Args('input', { type: () => GetPresignedUrlInput }) input: GetPresignedUrlInput, + @CurrentUser() user: User, ) { - return this.ojoiApplicationService.getPresignedUrl(input) + return this.ojoiApplicationService.getPresignedUrl(input, user) } @Mutation(() => AddApplicationAttachmentResponse, { @@ -79,8 +84,9 @@ export class OfficialJournalOfIcelandApplicationResolver { addAttachment( @Args('input', { type: () => AddApplicationAttachmentInput }) input: AddApplicationAttachmentInput, + @CurrentUser() user: User, ) { - return this.ojoiApplicationService.addApplicationAttachment(input) + return this.ojoiApplicationService.addApplicationAttachment(input, user) } @Query(() => GetApplicationAttachmentsResponse, { @@ -89,8 +95,9 @@ export class OfficialJournalOfIcelandApplicationResolver { getAttachments( @Args('input', { type: () => GetApplicationAttachmentInput }) input: AddApplicationAttachmentInput, + @CurrentUser() user: User, ) { - return this.ojoiApplicationService.getApplicationAttachments(input) + return this.ojoiApplicationService.getApplicationAttachments(input, user) } @Mutation(() => AddApplicationAttachmentResponse, { @@ -99,7 +106,8 @@ export class OfficialJournalOfIcelandApplicationResolver { deleteAttachment( @Args('input', { type: () => DeleteApplicationAttachmentInput }) input: DeleteApplicationAttachmentInput, + @CurrentUser() user: User, ) { - return this.ojoiApplicationService.deleteApplicationAttachment(input) + return this.ojoiApplicationService.deleteApplicationAttachment(input, user) } } diff --git a/libs/api/domains/official-journal-of-iceland-application/src/lib/ojoiApplication.service.ts b/libs/api/domains/official-journal-of-iceland-application/src/lib/ojoiApplication.service.ts index e8b2adc91172..89b074ee1eae 100644 --- a/libs/api/domains/official-journal-of-iceland-application/src/lib/ojoiApplication.service.ts +++ b/libs/api/domains/official-journal-of-iceland-application/src/lib/ojoiApplication.service.ts @@ -16,6 +16,7 @@ import { GetApplicationAttachmentInput } from '../models/getApplicationAttachmen import { DeleteApplicationAttachmentInput } from '../models/deleteApplicationAttachment.input' import { LOGGER_PROVIDER } from '@island.is/logging' import type { Logger } from '@island.is/logging' +import { User } from '@island.is/auth-nest-tools' const LOG_CATEGORY = 'official-journal-of-iceland-application' @@ -27,72 +28,92 @@ export class OfficialJournalOfIcelandApplicationService { private readonly ojoiApplicationService: OfficialJournalOfIcelandApplicationClientService, ) {} - async getComments(input: GetCommentsInput) { - return this.ojoiApplicationService.getComments(input) + async getComments(input: GetCommentsInput, user: User) { + return this.ojoiApplicationService.getComments(input, user) } - async postComment(input: PostCommentInput) { - const success = this.ojoiApplicationService.postComment({ - id: input.id, - postApplicationComment: { - comment: input.comment, + async postComment(input: PostCommentInput, user: User) { + const success = this.ojoiApplicationService.postComment( + { + id: input.id, + postApplicationComment: { + comment: input.comment, + }, }, - }) + user, + ) return { success, } } - async getPdfUrl(id: string) { - return this.ojoiApplicationService.getPdfUrl({ - id, - }) + async getPdfUrl(id: string, user: User) { + return this.ojoiApplicationService.getPdfUrl( + { + id, + }, + user, + ) } - async postApplication(input: PostApplicationInput): Promise { - return this.ojoiApplicationService.postApplication(input) + async postApplication( + input: PostApplicationInput, + user: User, + ): Promise { + return this.ojoiApplicationService.postApplication(input, user) } - async getPrice(id: string) { - return this.ojoiApplicationService.getPrice({ - id, - }) + async getPrice(id: string, user: User) { + return this.ojoiApplicationService.getPrice( + { + id, + }, + user, + ) } async getPresignedUrl( input: GetPresignedUrlInput, + user: User, ): Promise { const attachmentType = mapPresignedUrlType(input.attachmentType) - return this.ojoiApplicationService.getPresignedUrl({ - id: input.applicationId, - type: attachmentType, - getPresignedUrlBody: { - fileName: input.fileName, - fileType: input.fileType, + return this.ojoiApplicationService.getPresignedUrl( + { + id: input.applicationId, + type: attachmentType, + getPresignedUrlBody: { + fileName: input.fileName, + fileType: input.fileType, + }, }, - }) + user, + ) } async addApplicationAttachment( input: AddApplicationAttachmentInput, + user: User, ): Promise { try { const attachmentType = mapAttachmentType(input.attachmentType) - this.ojoiApplicationService.addApplicationAttachment({ - id: input.applicationId, - type: attachmentType, - postApplicationAttachmentBody: { - fileName: input.fileName, - originalFileName: input.originalFileName, - fileFormat: input.fileFormat, - fileExtension: input.fileExtension, - fileLocation: input.fileLocation, - fileSize: input.fileSize, + this.ojoiApplicationService.addApplicationAttachment( + { + id: input.applicationId, + type: attachmentType, + postApplicationAttachmentBody: { + fileName: input.fileName, + originalFileName: input.originalFileName, + fileFormat: input.fileFormat, + fileExtension: input.fileExtension, + fileLocation: input.fileLocation, + fileSize: input.fileSize, + }, }, - }) + user, + ) return { success: true, @@ -109,19 +130,31 @@ export class OfficialJournalOfIcelandApplicationService { } } - async getApplicationAttachments(input: GetApplicationAttachmentInput) { - return this.ojoiApplicationService.getApplicationAttachments({ - id: input.applicationId, - type: mapGetAttachmentType(input.attachmentType), - }) + async getApplicationAttachments( + input: GetApplicationAttachmentInput, + user: User, + ) { + return this.ojoiApplicationService.getApplicationAttachments( + { + id: input.applicationId, + type: mapGetAttachmentType(input.attachmentType), + }, + user, + ) } - async deleteApplicationAttachment(input: DeleteApplicationAttachmentInput) { + async deleteApplicationAttachment( + input: DeleteApplicationAttachmentInput, + user: User, + ) { try { - await this.ojoiApplicationService.deleteApplicationAttachment({ - id: input.applicationId, - key: input.key, - }) + await this.ojoiApplicationService.deleteApplicationAttachment( + { + id: input.applicationId, + key: input.key, + }, + user, + ) return { success: true } } catch (error) { diff --git a/libs/application/template-api-modules/src/lib/modules/templates/official-journal-of-iceland/official-journal-of-iceland.service.ts b/libs/application/template-api-modules/src/lib/modules/templates/official-journal-of-iceland/official-journal-of-iceland.service.ts index 0d8c5a82aadd..c652f9330b3a 100644 --- a/libs/application/template-api-modules/src/lib/modules/templates/official-journal-of-iceland/official-journal-of-iceland.service.ts +++ b/libs/application/template-api-modules/src/lib/modules/templates/official-journal-of-iceland/official-journal-of-iceland.service.ts @@ -34,9 +34,12 @@ export class OfficialJournalOfIcelandTemaplateService extends BaseTemplateApiSer async postApplication({ application, auth }: Props): Promise { try { - return await this.ojoiApplicationService.postApplication({ - id: application.id, - }) + return await this.ojoiApplicationService.postApplication( + { + id: application.id, + }, + auth, + ) } catch (error) { return false } diff --git a/libs/clients/official-journal-of-iceland/application/src/lib/ojoiApplicationClient.config.ts b/libs/clients/official-journal-of-iceland/application/src/lib/ojoiApplicationClient.config.ts index a316d887e3dd..edaf99d628cc 100644 --- a/libs/clients/official-journal-of-iceland/application/src/lib/ojoiApplicationClient.config.ts +++ b/libs/clients/official-journal-of-iceland/application/src/lib/ojoiApplicationClient.config.ts @@ -5,8 +5,8 @@ const schema = z.object({ xRoadServicePath: z.string(), fetch: z.object({ timeout: z.number().int(), - scope: z.array(z.string()), }), + scope: z.array(z.string()), }) export const OfficialJournalOfIcelandApplicationClientConfig = defineConfig< @@ -19,9 +19,9 @@ export const OfficialJournalOfIcelandApplicationClientConfig = defineConfig< 'XROAD_OFFICIAL_JOURNAL_APPLICATION_PATH', 'IS-DEV/GOV/10014/DMR-Protected/official-journal-application', ), + scope: ['api_resource.scope'], fetch: { timeout: 10000, - scope: [], }, }), }) diff --git a/libs/clients/official-journal-of-iceland/application/src/lib/ojoiApplicationClient.provider.ts b/libs/clients/official-journal-of-iceland/application/src/lib/ojoiApplicationClient.provider.ts index fdee03f0ac3e..23af8f9670db 100644 --- a/libs/clients/official-journal-of-iceland/application/src/lib/ojoiApplicationClient.provider.ts +++ b/libs/clients/official-journal-of-iceland/application/src/lib/ojoiApplicationClient.provider.ts @@ -6,7 +6,7 @@ import { import { createEnhancedFetch } from '@island.is/clients/middlewares' import { OfficialJournalOfIcelandApplicationClientConfig } from './ojoiApplicationClient.config' import { ConfigType } from '@nestjs/config' -import { XRoadConfig } from '@island.is/nest/config' +import { IdsClientConfig, XRoadConfig } from '@island.is/nest/config' export const OfficialJournalOfIcelandApplicationClientApiProvider: Provider = { @@ -16,11 +16,21 @@ export const OfficialJournalOfIcelandApplicationClientApiProvider: Provider, + idsClientConfig: ConfigType, ) => { return new OfficialJournalOfIcelandApplicationApi( new Configuration({ fetchApi: createEnhancedFetch({ name: 'clients-official-journal-of-iceland-application', + autoAuth: idsClientConfig.isConfigured + ? { + mode: 'tokenExchange', + issuer: idsClientConfig.issuer, + clientId: idsClientConfig.clientId, + clientSecret: idsClientConfig.clientSecret, + scope: config.scope, + } + : undefined, organizationSlug: 'domsmalaraduneytid', }), basePath: `${xroadConfig.xRoadBasePath}/r1/${config.xRoadServicePath}`, @@ -34,5 +44,6 @@ export const OfficialJournalOfIcelandApplicationClientApiProvider: Provider { try { - return await this.ojoiApplicationApi.getComments(params) + return await this.ojoiApplicationApiWithAuth(auth).getComments(params) } catch (error) { - console.log(error) - this.logger.error('Failed to get comments', { + this.logger.warn('Failed to get comments', { error, applicationId: params.id, category: LOG_CATEGORY, @@ -46,12 +51,12 @@ export class OfficialJournalOfIcelandApplicationClientService { } } - async postComment(params: PostCommentRequest): Promise { + async postComment(params: PostCommentRequest, auth: Auth): Promise { try { - await this.ojoiApplicationApi.postComment(params) + await this.ojoiApplicationApiWithAuth(auth).postComment(params) return true } catch (error) { - this.logger.error(`Failed to post comment: ${error.message}`, { + this.logger.warn(`Failed to post comment: ${error.message}`, { error, applicationId: params.id, category: LOG_CATEGORY, @@ -60,12 +65,15 @@ export class OfficialJournalOfIcelandApplicationClientService { } } - async postApplication(params: PostApplicationRequest): Promise { + async postApplication( + params: PostApplicationRequest, + auth: Auth, + ): Promise { try { - await this.ojoiApplicationApi.postApplication(params) + await this.ojoiApplicationApiWithAuth(auth).postApplication(params) return Promise.resolve(true) } catch (error) { - this.logger.error('Failed to post application', { + this.logger.warn('Failed to post application', { error, applicationId: params.id, category: LOG_CATEGORY, @@ -76,14 +84,20 @@ export class OfficialJournalOfIcelandApplicationClientService { async getPdfUrl( params: GetPdfUrlByApplicationIdRequest, + auth: Auth, ): Promise { - return await this.ojoiApplicationApi.getPdfUrlByApplicationId(params) - } - - async getPdf(params: GetPdfByApplicationIdRequest): Promise { - const streamableFile = await this.ojoiApplicationApi.getPdfByApplicationId( + return await this.ojoiApplicationApiWithAuth(auth).getPdfUrlByApplicationId( params, ) + } + + async getPdf( + params: GetPdfByApplicationIdRequest, + auth: Auth, + ): Promise { + const streamableFile = await this.ojoiApplicationApiWithAuth( + auth, + ).getPdfByApplicationId(params) const isStreamable = ( streamableFile: any, @@ -106,11 +120,14 @@ export class OfficialJournalOfIcelandApplicationClientService { return Buffer.concat(chunks) } - async getPrice(params: GetPriceRequest): Promise { + async getPrice( + params: GetPriceRequest, + auth: Auth, + ): Promise { try { - return await this.ojoiApplicationApi.getPrice(params) + return await this.ojoiApplicationApiWithAuth(auth).getPrice(params) } catch (error) { - this.logger.error('Failed to get price', { + this.logger.warn('Failed to get price', { applicationId: params.id, error, category: LOG_CATEGORY, @@ -122,23 +139,43 @@ export class OfficialJournalOfIcelandApplicationClientService { } async getPresignedUrl( params: GetPresignedUrlRequest, + auth: Auth, ): Promise { - return await this.ojoiApplicationApi.getPresignedUrl(params) + return await this.ojoiApplicationApiWithAuth(auth).getPresignedUrl(params) } async addApplicationAttachment( params: AddApplicationAttachmentRequest, + auth: Auth, ): Promise { - await this.ojoiApplicationApi.addApplicationAttachment(params) + try { + await this.ojoiApplicationApiWithAuth(auth).addApplicationAttachment( + params, + ) + } catch (error) { + this.logger.warn('Failed to add application attachment', { + category: LOG_CATEGORY, + applicationId: params.id, + }) + throw error + } } - async getApplicationAttachments(params: GetApplicationAttachmentsRequest) { - return this.ojoiApplicationApi.getApplicationAttachments(params) + async getApplicationAttachments( + params: GetApplicationAttachmentsRequest, + auth: Auth, + ) { + return this.ojoiApplicationApiWithAuth(auth).getApplicationAttachments( + params, + ) } async deleteApplicationAttachment( params: DeleteApplicationAttachmentRequest, + auth: Auth, ) { - await this.ojoiApplicationApi.deleteApplicationAttachment(params) + await this.ojoiApplicationApiWithAuth(auth).deleteApplicationAttachment( + params, + ) } } From bced7f921d3589fb276b00e0ba792edbebe837a9 Mon Sep 17 00:00:00 2001 From: albinagu <47886428+albinagu@users.noreply.github.com> Date: Fri, 20 Sep 2024 10:28:22 +0000 Subject: [PATCH 03/18] fix(service-portal): owner UI updates + paper signees (#16067) * fix(service-portal): UI updates + paper signees * add constituency functionality * paper signees functionality --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../src/hooks/graphql/mutations.ts | 21 ++ .../src/hooks/graphql/queries.ts | 8 + .../signature-collection/src/hooks/index.ts | 17 ++ .../signature-collection/src/lib/constants.ts | 10 - .../signature-collection/src/lib/messages.ts | 113 +++++++---- .../OwnerView/AddConstituency/index.tsx | 69 +++++-- .../ViewList/Signees/PaperSignees.tsx | 188 ++++++++++++++++++ .../{Signees.tsx => Signees/index.tsx} | 68 ++++--- .../OwnerView/ViewList/index.tsx | 38 +--- .../screens/Parliamentary/OwnerView/index.tsx | 124 +++++++++++- .../OwnerView}/CancelCollection/index.tsx | 18 +- .../screens/Presidential/OwnerView/index.tsx | 2 +- .../src/screens/shared/SignedList/index.tsx | 18 +- .../src/screens/shared/SigneeView/index.tsx | 3 +- 14 files changed, 538 insertions(+), 159 deletions(-) create mode 100644 libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees/PaperSignees.tsx rename libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/{Signees.tsx => Signees/index.tsx} (69%) rename libs/service-portal/signature-collection/src/screens/{shared => Presidential/OwnerView}/CancelCollection/index.tsx (83%) diff --git a/libs/service-portal/signature-collection/src/hooks/graphql/mutations.ts b/libs/service-portal/signature-collection/src/hooks/graphql/mutations.ts index dd9bb068e793..8fb53c33cfb7 100644 --- a/libs/service-portal/signature-collection/src/hooks/graphql/mutations.ts +++ b/libs/service-portal/signature-collection/src/hooks/graphql/mutations.ts @@ -18,3 +18,24 @@ export const unSignList = gql` } } ` +export const addConstituency = gql` + mutation SignatureCollectionAddAreas( + $inputAdd: SignatureCollectionAddListsInput! + ) { + signatureCollectionAddAreas(input: $inputAdd) { + success + reasons + } + } +` + +export const uploadPaperSignature = gql` + mutation SignatureCollectionUploadPaperSignature( + $input: SignatureCollectionUploadPaperSignatureInput! + ) { + signatureCollectionUploadPaperSignature(input: $input) { + success + reasons + } + } +` diff --git a/libs/service-portal/signature-collection/src/hooks/graphql/queries.ts b/libs/service-portal/signature-collection/src/hooks/graphql/queries.ts index 55a7b6a24cb9..45d50fdbe81c 100644 --- a/libs/service-portal/signature-collection/src/hooks/graphql/queries.ts +++ b/libs/service-portal/signature-collection/src/hooks/graphql/queries.ts @@ -45,6 +45,7 @@ export const GetListSignatures = gql` isDigital valid created + pageNumber } } ` @@ -70,6 +71,7 @@ export const GetSignedList = gql` collectionId canUnsign slug + signedDate } } ` @@ -155,3 +157,9 @@ export const GetCurrentCollection = gql` } } ` + +export const GetCanSign = gql` + query Query($input: SignatureCollectionCanSignInput!) { + signatureCollectionCanSign(input: $input) + } +` diff --git a/libs/service-portal/signature-collection/src/hooks/index.ts b/libs/service-portal/signature-collection/src/hooks/index.ts index d8246c12763c..9c3800136b8e 100644 --- a/libs/service-portal/signature-collection/src/hooks/index.ts +++ b/libs/service-portal/signature-collection/src/hooks/index.ts @@ -7,6 +7,7 @@ import { GetSignedList, GetListsForOwner, GetCurrentCollection, + GetCanSign, } from './graphql/queries' import { SignatureCollectionListBase, @@ -147,3 +148,19 @@ export const useGetCurrentCollection = () => { refetchCurrentCollection, } } + +export const useGetCanSign = (signeeId: string, isValidId: boolean) => { + const { data: getCanSignData, loading: loadingCanSign } = useQuery( + GetCanSign, + { + variables: { + input: { + signeeNationalId: signeeId, + }, + }, + skip: !signeeId || signeeId.length !== 10 || !isValidId, + }, + ) + const canSign = getCanSignData?.signatureCollectionCanSign ?? false + return { canSign, loadingCanSign } +} diff --git a/libs/service-portal/signature-collection/src/lib/constants.ts b/libs/service-portal/signature-collection/src/lib/constants.ts index 04eb57fdee3a..8e28b87d3fe3 100644 --- a/libs/service-portal/signature-collection/src/lib/constants.ts +++ b/libs/service-portal/signature-collection/src/lib/constants.ts @@ -2,13 +2,3 @@ export const CollectionType = { Presidential: 'Forsetakosningar', Parliamentary: 'Alþingiskosningar', } - -// will be fetched later on -export const constituencies = [ - 'Norðvesturkjördæmi', - 'Norðausturkjördæmi', - 'Suðurkjördæmi', - 'Suðvesturkjördæmi', - 'Reykjavíkurkjördæmi suður', - 'Reykjavíkurkjördæmi norður', -] diff --git a/libs/service-portal/signature-collection/src/lib/messages.ts b/libs/service-portal/signature-collection/src/lib/messages.ts index 7cad88479236..0518d12283d8 100644 --- a/libs/service-portal/signature-collection/src/lib/messages.ts +++ b/libs/service-portal/signature-collection/src/lib/messages.ts @@ -51,7 +51,7 @@ export const m = defineMessages({ }, copyLinkDescription: { id: 'sp.signatureCollection:copyLinkDescription', - defaultMessage: 'Hér getur þú afritað hlekk á þitt framboð til að deila.', + defaultMessage: 'Hér getur þú afritað hlekk á þitt framboð til að deila', description: '', }, copyLinkSuccess: { @@ -99,6 +99,11 @@ export const m = defineMessages({ defaultMessage: 'Meðmæli lesin inn', description: '', }, + digitalSignature: { + id: 'sp.signatureCollection:digitalSignature', + defaultMessage: 'Skrifað undir: ', + description: '', + }, signatureIsInvalid: { id: 'sp.signatureCollection:signatureIsInvalid', defaultMessage: 'Ógilt meðmæli', @@ -181,7 +186,12 @@ export const m = defineMessages({ }, cancelCollectionModalConfirmButton: { id: 'sp.signatureCollection:modalConfirmButton', - defaultMessage: 'Já, hætta við söfnun meðmæla', + defaultMessage: 'Já, hætta við', + description: '', + }, + cancelCollectionModalCancelButton: { + id: 'sp.signatureCollection:cancelCollectionModalCancelButton', + defaultMessage: 'Nei, hætta við', description: '', }, cancelCollectionModalToastError: { @@ -256,6 +266,61 @@ export const m = defineMessages({ defaultMessage: 'Heimilisfang', description: '', }, + paperSigneesHeader: { + id: 'sp.signatureCollection:paperSigneesHeader', + defaultMessage: 'Skrá meðmæli af blaði', + description: '', + }, + paperSigneesClearButton: { + id: 'sp.signatureCollection:paperSigneesClearButton', + defaultMessage: 'Hreinsa', + description: '', + }, + paperNumber: { + id: 'sp.signatureCollection:paperNumber', + defaultMessage: 'Blaðsíðunúmer', + description: '', + }, + paperSigneeName: { + id: 'sp.signatureCollection:paperSigneeName', + defaultMessage: 'Nafn meðmælanda', + description: '', + }, + signPaperSigneeButton: { + id: 'sp.signatureCollection:signPaperSigneeButton', + defaultMessage: 'Skrá meðmæli á lista', + description: '', + }, + paperSigneeTypoTitle: { + id: 'sp.signatureCollection:paperSigneeTypoTitle', + defaultMessage: 'Kennitala ekki á réttu formi', + description: '', + }, + paperSigneeTypoMessage: { + id: 'sp.signatureCollection:paperSigneeTypoMessage', + defaultMessage: 'Vinsamlegast athugið kennitöluna og reynið aftur', + description: '', + }, + paperSigneeCantSignTitle: { + id: 'sp.signatureCollection:paperSigneeCantSignTitle', + defaultMessage: 'Ekki er hægt að skrá meðmæli', + description: '', + }, + paperSigneeCantSignMessage: { + id: 'sp.signatureCollection:paperSigneeCantSign', + defaultMessage: 'Kennitala uppfyllir ekki skilyrði fyrir að skrá meðmæli', + description: '', + }, + paperSigneeSuccess: { + id: 'sp.signatureCollection:paperSigneeSuccess', + defaultMessage: 'Meðmæli skráð', + description: '', + }, + paperSigneeError: { + id: 'sp.signatureCollection:paperSigneeError', + defaultMessage: 'Ekki tókst að skrá meðmæli', + description: '', + }, /* Parliamentary */ parliamentaryElectionsTitle: { @@ -279,16 +344,6 @@ export const m = defineMessages({ defaultMessage: 'Þjóðskrá Íslands hefur umsjón með gögnum um meðmælasöfnun.', }, - managers: { - id: 'sp.signatureCollection:managers', - defaultMessage: 'Ábyrgðaraðilar', - description: '', - }, - addManager: { - id: 'sp.signatureCollection:addManager', - defaultMessage: 'Bæta við ábyrgðaraðila', - description: '', - }, supervisors: { id: 'sp.signatureCollection:supervisors', defaultMessage: 'Umsjónaraðilar', @@ -299,11 +354,6 @@ export const m = defineMessages({ defaultMessage: 'Bæta við', description: '', }, - addSupervisor: { - id: 'sp.signatureCollection:addSupervisor', - defaultMessage: 'Bæta við umsjónaraðila', - description: '', - }, personName: { id: 'sp.signatureCollection:personName', defaultMessage: 'Nafn', @@ -340,31 +390,14 @@ export const m = defineMessages({ 'Veldu viðeigandi kjördæmi sem þú vilt stofna meðmælendasöfnun í.', description: '', }, - addConstituencyAlertInfo: { - id: 'sp.signatureCollection:addConstituencyAlertInfo', - defaultMessage: - 'Athugið að skrá þarf viðeigandi ábyrgðar-/umsjónaraðila á yfirlitssíðu fyrir ný kjördæmi.', - description: '', - }, - deleteManager: { - id: 'sp.signatureCollection:deleteManager', - defaultMessage: 'Eyða umsjónaraðila', - description: '', - }, - deleteManagerDescription: { - id: 'sp.signatureCollection:deleteManagerDescription', - defaultMessage: - 'Þú ert að fara að taka Nafna Nafnason af lista yfir umsjónaraðila. Ertu viss um að þú viljir halda áfram?', - description: '', - }, - delete: { - id: 'sp.signatureCollection:delete', - defaultMessage: 'Eyða', + addConstituencySuccess: { + id: 'sp.signatureCollection:addConstituencySuccess', + defaultMessage: 'Kjördæmi bætt við', description: '', }, - save: { - id: 'sp.signatureCollection:save', - defaultMessage: 'Vista', + addConstituencyError: { + id: 'sp.signatureCollection:addConstituencyError', + defaultMessage: 'Ekki tókst að bæta við kjördæmi', description: '', }, }) diff --git a/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/AddConstituency/index.tsx b/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/AddConstituency/index.tsx index 4820dcef6996..c6119aa3ed74 100644 --- a/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/AddConstituency/index.tsx +++ b/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/AddConstituency/index.tsx @@ -1,26 +1,63 @@ import { useState } from 'react' -import { Box, Button, Text, Checkbox } from '@island.is/island-ui/core' +import { Box, Button, Text, Checkbox, toast } from '@island.is/island-ui/core' import { Modal } from '@island.is/service-portal/core' import { useLocale } from '@island.is/localization' import { m } from '../../../../lib/messages' -import { constituencies } from '../../../../lib/constants' -import { SignatureCollectionList } from '@island.is/api/schema' +import { + SignatureCollection, + SignatureCollectionArea, + SignatureCollectionList, +} from '@island.is/api/schema' +import { addConstituency } from '../../../../hooks/graphql/mutations' +import { useMutation } from '@apollo/client' const AddConstituencyModal = ({ lists, + collection, + candidateId, + refetch, }: { lists: SignatureCollectionList[] + collection: SignatureCollection + candidateId: string + refetch: () => void }) => { const { formatMessage } = useLocale() - const listTitles = lists.map((l) => l.title) - const filteredConstituencies = constituencies.filter( - (c) => !listTitles.some((title) => title.includes(c)), - ) + const currentConstituencies = lists.map( + (l) => l.area, + ) as SignatureCollectionArea[] + const filteredConstituencies = collection.areas.filter( + (cc) => !currentConstituencies.some((c) => cc.name === c.name), + ) as SignatureCollectionArea[] + const [modalIsOpen, setModalIsOpen] = useState(false) const [selectedConstituencies, setSelectedConstituencies] = useState< string[] >([]) + const [addNewConstituency, { loading }] = useMutation(addConstituency, { + onCompleted: () => { + setModalIsOpen(false) + refetch() + toast.success(formatMessage(m.addConstituencySuccess)) + }, + onError: () => { + toast.error(formatMessage(m.addConstituencyError)) + }, + }) + + const onAddConstituency = async () => { + addNewConstituency({ + variables: { + inputAdd: { + collectionId: collection?.id, + areaIds: selectedConstituencies, + candidateId: candidateId, + }, + }, + }) + } + return ( + diff --git a/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees/PaperSignees.tsx b/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees/PaperSignees.tsx new file mode 100644 index 000000000000..f101e7bd3d80 --- /dev/null +++ b/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees/PaperSignees.tsx @@ -0,0 +1,188 @@ +import { + Box, + Text, + Button, + GridRow, + GridColumn, + GridContainer, + AlertMessage, + Input, +} from '@island.is/island-ui/core' +import { useLocale, useNamespaces } from '@island.is/localization' +import { useIdentityQuery } from '@island.is/service-portal/graphql' +import * as nationalId from 'kennitala' +import { useEffect, useState } from 'react' +import { InputController } from '@island.is/shared/form-fields' +import { useForm } from 'react-hook-form' +import { m } from '../../../../../lib/messages' +import { useGetCanSign } from '../../../../../hooks' +import { useMutation } from '@apollo/client' +import { uploadPaperSignature } from '../../../../../hooks/graphql/mutations' +import { toast } from 'react-toastify' + +export const PaperSignees = ({ + listId, + refetchSignees, +}: { + listId: string + refetchSignees: () => void +}) => { + useNamespaces('sp.signatureCollection') + const { formatMessage } = useLocale() + const { control, reset } = useForm() + + const [nationalIdInput, setNationalIdInput] = useState('') + const [nationalIdTypo, setNationalIdTypo] = useState(false) + const [page, setPage] = useState('') + const [name, setName] = useState('') + + /* identity & canSign fetching logic */ + const { data, loading } = useIdentityQuery({ + variables: { input: { nationalId: nationalIdInput } }, + skip: nationalIdInput.length !== 10 || !nationalId.isValid(nationalIdInput), + onCompleted: (data) => setName(data.identity?.name || ''), + }) + const { canSign, loadingCanSign } = useGetCanSign( + nationalIdInput, + nationalId.isValid(nationalIdInput), + ) + + useEffect(() => { + if (nationalIdInput.length === 10) { + setNationalIdTypo( + !nationalId.isValid(nationalIdInput) || + (!loading && !data?.identity?.name), + ) + } else { + setName('') + setNationalIdTypo(false) + } + }, [nationalIdInput, loading, data]) + + /* upload paper signature logic */ + const [upload, { loading: uploadingPaperSignature }] = useMutation( + uploadPaperSignature, + { + variables: { + input: { + listId: listId, + nationalId: nationalIdInput, + pageNumber: Number(page), + }, + }, + onCompleted: () => { + toast.success(formatMessage(m.paperSigneeSuccess)) + refetchSignees() + }, + onError: () => { + toast.error(formatMessage(m.paperSigneeError)) + }, + }, + ) + + const onClearForm = () => { + reset() // resets nationalId field + setNationalIdTypo(false) + setName('') + } + + return ( + + + + {formatMessage(m.paperSigneesHeader)} + + + + + + + + + + + { + setNationalIdInput(e.target.value.replace(/\W/g, '')) + }} + error={nationalIdTypo ? ' ' : undefined} + loading={loading || loadingCanSign} + icon={canSign ? 'checkmark' : undefined} + /> + + + setPage(e.target.value)} + /> + + + + + + + + + + + + + {nationalIdTypo && ( + + + + )} + {name && !loadingCanSign && !canSign && ( + + + + )} + + ) +} diff --git a/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees.tsx b/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees/index.tsx similarity index 69% rename from libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees.tsx rename to libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees/index.tsx index 49031cc603aa..581ffb77b6be 100644 --- a/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees.tsx +++ b/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees/index.tsx @@ -4,28 +4,28 @@ import { Table as T, Pagination, FilterInput, + Icon, } from '@island.is/island-ui/core' import { useLocale, useNamespaces } from '@island.is/localization' -import { m } from '../../../../lib/messages' +import { m } from '../../../../../lib/messages' import format from 'date-fns/format' import { useEffect, useState } from 'react' -import { useGetListSignees } from '../../../../hooks' -import { useLocation } from 'react-router-dom' +import { useGetListSignees } from '../../../../../hooks' +import { useParams } from 'react-router-dom' import { format as formatNationalId } from 'kennitala' -import { SkeletonTable } from '../../../../skeletons' +import { SkeletonTable } from '../../../../../skeletons' import { SignatureCollectionSignature as Signature } from '@island.is/api/schema' +import { PaperSignees } from './PaperSignees' +import sortBy from 'lodash/sortBy' const Signees = () => { useNamespaces('sp.signatureCollection') const { formatMessage } = useLocale() - const { pathname } = useLocation() - const listId = pathname.replace( - '/min-gogn/listar/althingis-medmaelasofnun/', - '', - ) + const { id } = useParams() as { id: string } const [searchTerm, setSearchTerm] = useState('') - const { listSignees, loadingSignees } = useGetListSignees(listId) + const { listSignees, loadingSignees, refetchListSignees } = + useGetListSignees(id) const [signees, setSignees] = useState(listSignees) const [page, setPage] = useState(1) @@ -33,7 +33,11 @@ const Signees = () => { useEffect(() => { if (!loadingSignees && listSignees.length) { - setSignees(listSignees) + setSignees( + sortBy(listSignees, (item) => { + return item.created + }).reverse(), + ) } }, [listSignees]) @@ -56,30 +60,25 @@ const Signees = () => { return ( {formatMessage(m.signeesHeader)} - - - setSearchTerm(v)} - placeholder={formatMessage(m.searchInListPlaceholder)} - backgroundColor="white" - /> - + + setSearchTerm(v)} + placeholder={formatMessage(m.searchInListPlaceholder)} + backgroundColor="white" + /> {!loadingSignees ? ( signees.length > 0 ? ( - + {formatMessage(m.signeeDate)} {formatMessage(m.signeeName)} {formatMessage(m.signeeNationalId)} + @@ -88,7 +87,7 @@ const Signees = () => { .map((s: Signature) => { return ( - + {format(new Date(), 'dd.MM.yyyy')} @@ -97,13 +96,25 @@ const Signees = () => { {formatNationalId(s.signee.nationalId)} + + {!s.isDigital && ( + + + {s.pageNumber} + + )} + ) })} - + { ) : ( )} + ) } diff --git a/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/index.tsx b/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/index.tsx index b99c8b73b533..89bf289b13fb 100644 --- a/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/index.tsx +++ b/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/index.tsx @@ -1,17 +1,15 @@ -import { Box, Button, Stack, Text, toast } from '@island.is/island-ui/core' +import { Box, Stack, Text } from '@island.is/island-ui/core' import { useLocale, useNamespaces } from '@island.is/localization' import { m } from '../../../../lib/messages' import { useParams } from 'react-router-dom' import { useGetSignatureList } from '../../../../hooks' import format from 'date-fns/format' import Signees from './Signees' -import CancelCollection from '../../../shared/CancelCollection' -import copyToClipboard from 'copy-to-clipboard' const ViewList = () => { useNamespaces('sp.signatureCollection') const { formatMessage } = useLocale() - const { id } = useParams() + const { id } = useParams() as { id: string } const { listInfo, loadingList } = useGetSignatureList(id || '') return ( @@ -54,39 +52,7 @@ const ViewList = () => { )} - - - - {formatMessage(m.copyLinkDescription)} - - - - - - )} diff --git a/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/index.tsx b/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/index.tsx index faa2ec4c2528..659882231e88 100644 --- a/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/index.tsx +++ b/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/index.tsx @@ -5,16 +5,27 @@ import { Text, Table as T, Tooltip, + DialogPrompt, + Tag, + Icon, + toast, + Button, } from '@island.is/island-ui/core' import { useNavigate } from 'react-router-dom' import { SignatureCollectionPaths } from '../../../lib/paths' import { useLocale } from '@island.is/localization' import { m } from '../../../lib/messages' import AddConstituency from './AddConstituency' -import { SignatureCollectionList } from '@island.is/api/schema' +import { + SignatureCollectionList, + SignatureCollectionSuccess, +} from '@island.is/api/schema' import { OwnerParliamentarySkeleton } from '../../../skeletons' import { useGetListsForOwner } from '../../../hooks' import { SignatureCollection } from '@island.is/api/schema' +import { useMutation } from '@apollo/client' +import { cancelCollectionMutation } from '../../../hooks/graphql/mutations' +import copyToClipboard from 'copy-to-clipboard' const OwnerView = ({ currentCollection, @@ -23,10 +34,33 @@ const OwnerView = ({ }) => { const navigate = useNavigate() const { formatMessage } = useLocale() - const { listsForOwner, loadingOwnerLists } = useGetListsForOwner( - currentCollection?.id || '', + const { listsForOwner, loadingOwnerLists, refetchListsForOwner } = + useGetListsForOwner(currentCollection?.id || '') + + const [cancelCollection] = useMutation( + cancelCollectionMutation, + { + onCompleted: () => { + toast.success(formatMessage(m.cancelCollectionModalToastSuccess)) + refetchListsForOwner() + }, + onError: () => { + toast.error(formatMessage(m.cancelCollectionModalToastError)) + }, + }, ) + const onCancelCollection = (listId: string) => { + cancelCollection({ + variables: { + input: { + collectionId: currentCollection?.id ?? '', + listIds: listId, + }, + }, + }) + } + return ( @@ -39,11 +73,15 @@ const OwnerView = ({ color="blue400" /> - {/* If the number of lists is equal to 6, it means that - lists have been created in all of the constituencies */} - {listsForOwner.length < 6 && ( - - )} + {!loadingOwnerLists && + listsForOwner?.length < currentCollection?.areas.length && ( + + )} {loadingOwnerLists ? ( @@ -54,12 +92,13 @@ const OwnerView = ({ ( + + + + + + } + onConfirm={() => { + onCancelCollection(list.id) + }} + buttonTextConfirm={formatMessage( + m.cancelCollectionModalConfirmButton, + )} + buttonPropsConfirm={{ + variant: 'primary', + colorScheme: 'destructive', + }} + buttonTextCancel={formatMessage( + m.cancelCollectionModalCancelButton, + )} + /> + ), + }} /> )) @@ -110,6 +186,36 @@ const OwnerView = ({ + + + {formatMessage(m.copyLinkDescription)} + + + + + ) } diff --git a/libs/service-portal/signature-collection/src/screens/shared/CancelCollection/index.tsx b/libs/service-portal/signature-collection/src/screens/Presidential/OwnerView/CancelCollection/index.tsx similarity index 83% rename from libs/service-portal/signature-collection/src/screens/shared/CancelCollection/index.tsx rename to libs/service-portal/signature-collection/src/screens/Presidential/OwnerView/CancelCollection/index.tsx index 08b0cc27a325..8e11940926fd 100644 --- a/libs/service-portal/signature-collection/src/screens/shared/CancelCollection/index.tsx +++ b/libs/service-portal/signature-collection/src/screens/Presidential/OwnerView/CancelCollection/index.tsx @@ -1,31 +1,23 @@ import { Box, Button, Text, toast } from '@island.is/island-ui/core' import { useLocale, useNamespaces } from '@island.is/localization' -import { m } from '../../../lib/messages' +import { m } from '../../../../lib/messages' import { Modal } from '@island.is/service-portal/core' import { useState } from 'react' -import { useGetCurrentCollection } from '../../../hooks' +import { useGetCurrentCollection } from '../../../../hooks' import { useMutation } from '@apollo/client' -import { cancelCollectionMutation } from '../../../hooks/graphql/mutations' -import { - SignatureCollectionCancelListsInput, - SignatureCollectionSuccess, -} from '@island.is/api/schema' +import { cancelCollectionMutation } from '../../../../hooks/graphql/mutations' +import { SignatureCollectionSuccess } from '@island.is/api/schema' -const CancelCollection = ({ listId }: { listId?: string }) => { +const CancelCollection = () => { useNamespaces('sp.signatureCollection') const { formatMessage } = useLocale() const [modalIsOpen, setModalIsOpen] = useState(false) const { currentCollection } = useGetCurrentCollection() - const input = {} as SignatureCollectionCancelListsInput - if (listId && !currentCollection?.isPresidential) { - input.listIds = [listId] - } const [cancelCollection, { loading }] = useMutation(cancelCollectionMutation, { variables: { input: { - ...input, collectionId: currentCollection?.id ?? '', }, }, diff --git a/libs/service-portal/signature-collection/src/screens/Presidential/OwnerView/index.tsx b/libs/service-portal/signature-collection/src/screens/Presidential/OwnerView/index.tsx index d159e7170c02..397e3776bbe4 100644 --- a/libs/service-portal/signature-collection/src/screens/Presidential/OwnerView/index.tsx +++ b/libs/service-portal/signature-collection/src/screens/Presidential/OwnerView/index.tsx @@ -17,7 +17,7 @@ import { useAuth } from '@island.is/auth/react' import copyToClipboard from 'copy-to-clipboard' import { SignatureCollection } from '@island.is/api/schema' import SignedList from '../../shared/SignedList' -import CancelCollection from '../../shared/CancelCollection' +import CancelCollection from './CancelCollection' const OwnerView = ({ currentCollection, diff --git a/libs/service-portal/signature-collection/src/screens/shared/SignedList/index.tsx b/libs/service-portal/signature-collection/src/screens/shared/SignedList/index.tsx index 4cf324e865ab..df7b95016cf0 100644 --- a/libs/service-portal/signature-collection/src/screens/shared/SignedList/index.tsx +++ b/libs/service-portal/signature-collection/src/screens/shared/SignedList/index.tsx @@ -70,12 +70,8 @@ const SignedList = ({ return ( Date: Fri, 20 Sep 2024 12:25:17 +0000 Subject: [PATCH 04/18] chore(j-s): Add ability to choose if there are civil claims in a case (#16091) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add radio buttons for hasCivilClaims choice and make decisions based on that * Remove files and reset civilDemands * Updates indictment screen validation --------- Co-authored-by: Guðjón Guðjónsson Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../IndictmentCaseFilesList.strings.ts | 5 ++ .../IndictmentCaseFilesList.tsx | 5 +- .../CaseFiles/CaseFiles.strings.ts | 2 +- .../Indictments/CaseFiles/CaseFiles.tsx | 58 +++++++----- .../Indictments/Indictment/Indictment.tsx | 78 ++++++++-------- .../Indictments/Processing/Processing.tsx | 88 ++++++++++++++++++- .../Processing/processing.strings.ts | 18 ++++ .../utils/hooks/useS3Upload/useS3Upload.ts | 4 +- .../judicial-system/web/src/utils/validate.ts | 14 ++- 9 files changed, 199 insertions(+), 73 deletions(-) diff --git a/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.strings.ts b/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.strings.ts index 134cae5bdca9..7e93be0a3dfc 100644 --- a/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.strings.ts +++ b/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.strings.ts @@ -28,4 +28,9 @@ export const strings = defineMessages({ description: 'Notaður sem titill á innsend gögn hluta á dómskjalaskjá í ákærum.', }, + civilClaimsTitle: { + id: 'judicial.system.core:indictment_case_files_list.civil_claims_title', + defaultMessage: 'Einkaréttarkröfur', + description: 'Notaður sem titill á dómskjalaskjá í ákærum.', + }, }) diff --git a/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.tsx b/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.tsx index 10d1b3b71023..f28a239cf977 100644 --- a/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.tsx +++ b/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.tsx @@ -230,14 +230,15 @@ const IndictmentCaseFilesList: FC = ({ )} ) : null} - {civilClaims && + {workingCase.hasCivilClaims && + civilClaims && civilClaims.length > 0 && (isDistrictCourtUser(user) || isProsecutionUser(user) || isDefenceUser(user)) && ( - {formatMessage(caseFiles.civilClaimSection)} + {formatMessage(strings.civilClaimsTitle)} { onRetry={(file) => handleRetry(file, updateUploadFile)} /> - + @@ -172,29 +177,34 @@ const CaseFiles = () => { onRetry={(file) => handleRetry(file, updateUploadFile)} /> - - - file.category === CaseFileCategory.CIVIL_CLAIM, - )} - accept={Object.values(fileExtensionWhitelist)} - header={formatMessage(strings.caseFiles.inputFieldLabel)} - buttonLabel={formatMessage(strings.caseFiles.buttonLabel)} - onChange={(files) => - handleUpload( - addUploadFiles(files, { - category: CaseFileCategory.CIVIL_CLAIM, - }), - updateUploadFile, - ) - } - onRemove={(file) => handleRemove(file, removeUploadFile)} - onRetry={(file) => handleRetry(file, updateUploadFile)} - /> - + {workingCase.hasCivilClaims && ( + + + file.category === CaseFileCategory.CIVIL_CLAIM, + )} + accept={Object.values(fileExtensionWhitelist)} + header={formatMessage(strings.caseFiles.inputFieldLabel)} + buttonLabel={formatMessage(strings.caseFiles.buttonLabel)} + onChange={(files) => + handleUpload( + addUploadFiles(files, { + category: CaseFileCategory.CIVIL_CLAIM, + }), + updateUploadFile, + ) + } + onRemove={(file) => handleRemove(file, removeUploadFile)} + onRetry={(file) => handleRetry(file, updateUploadFile)} + /> + + )} {isTrafficViolationCaseCheck && ( { /> - - - - - removeTabsValidateAndSet( - 'civilDemands', - event.target.value, - ['empty'], - setWorkingCase, - civilDemandsErrorMessage, - setCivilDemandsErrorMessage, - ) - } - onBlur={(event) => - validateAndSendToServer( - 'civilDemands', - event.target.value, - ['empty'], - workingCase, - updateCase, - setCivilDemandsErrorMessage, - ) - } - textarea - autoComplete="off" - required - rows={7} - autoExpand={{ on: true, maxHeight: 300 }} - /> - - + {workingCase.hasCivilClaims && ( + + + + + removeTabsValidateAndSet( + 'civilDemands', + event.target.value, + ['empty'], + setWorkingCase, + civilDemandsErrorMessage, + setCivilDemandsErrorMessage, + ) + } + onBlur={(event) => + validateAndSendToServer( + 'civilDemands', + event.target.value, + ['empty'], + workingCase, + updateCase, + setCivilDemandsErrorMessage, + ) + } + textarea + autoComplete="off" + required + rows={7} + autoExpand={{ on: true, maxHeight: 300 }} + /> + + + )} { isCaseUpToDate, refreshCase, } = useContext(FormContext) - const { updateCase, transitionCase } = useCase() + const { updateCase, transitionCase, setAndSendCaseToServer } = useCase() + const { handleRemove } = useS3Upload(workingCase.id) const { formatMessage } = useIntl() const { updateDefendant, updateDefendantState } = useDefendants() const router = useRouter() const isTrafficViolationCaseCheck = isTrafficViolationCase(workingCase) + const [hasCivilClaimsChoice, setHasCivilClaimsChoice] = useState() + const initialize = useCallback(async () => { if (!workingCase.court) { await updateCase(workingCase.id, { @@ -96,6 +101,36 @@ const Processing: FC = () => { [updateDefendantState, setWorkingCase, workingCase.id, updateDefendant], ) + const handleHasCivilClaimsChange = async (hasCivilClaims: boolean) => { + setHasCivilClaimsChoice(hasCivilClaims) + + setAndSendCaseToServer( + [{ hasCivilClaims, force: true }], + workingCase, + setWorkingCase, + ) + + if (hasCivilClaims === false) { + const civilClaims = workingCase.caseFiles?.filter( + (caseFile) => caseFile.category === CaseFileCategory.CIVIL_CLAIM, + ) + + if (!civilClaims) { + return + } + + setAndSendCaseToServer( + [{ civilDemands: null, force: true }], + workingCase, + setWorkingCase, + ) + + for (const civilClaim of civilClaims) { + handleRemove(civilClaim as UploadFile) + } + } + } + return ( { ))} )} - + + + + + + + handleHasCivilClaimsChange(true)} + checked={ + hasCivilClaimsChoice === true || + (hasCivilClaimsChoice === undefined && + workingCase.hasCivilClaims === true) + } + /> + + + handleHasCivilClaimsChange(false)} + checked={ + hasCivilClaimsChoice === false || + (hasCivilClaimsChoice === undefined && + workingCase.hasCivilClaims === false) + } + /> + + + + { ) const handleRemove = useCallback( - async (file: TUploadFile, callback: (file: TUploadFile) => void) => { + async (file: TUploadFile, callback?: (file: TUploadFile) => void) => { try { if (file.id) { const { data } = await remove(file.id) @@ -421,7 +421,7 @@ const useS3Upload = (caseId: string) => { throw new Error('Failed to delete file') } - callback(file) + callback && callback(file) } } catch { toast.error(formatMessage(strings.removeFailed)) diff --git a/apps/judicial-system/web/src/utils/validate.ts b/apps/judicial-system/web/src/utils/validate.ts index 003fa9471208..dae538e76314 100644 --- a/apps/judicial-system/web/src/utils/validate.ts +++ b/apps/judicial-system/web/src/utils/validate.ts @@ -267,15 +267,25 @@ export const isProcessingStepValidIndictments = ( return validate([[defendant.defendantPlea, ['empty']]]).isValid }) + const hasCivilClaimSelected = + workingCase.hasCivilClaims !== null && + workingCase.hasCivilClaims !== undefined + return Boolean( - workingCase.prosecutor && workingCase.court && defendantsAreValid(), + workingCase.prosecutor && + workingCase.court && + hasCivilClaimSelected && + defendantsAreValid(), ) } export const isTrafficViolationStepValidIndictments = ( workingCase: Case, ): boolean => { - return Boolean(workingCase.demands && workingCase.civilDemands) + return Boolean( + workingCase.demands && + (!workingCase.hasCivilClaims || workingCase.civilDemands), + ) } export const isPoliceDemandsStepValidRC = (workingCase: Case): boolean => { From 5a55fcf0a2fb091a360e11ce370a14330bd1cc51 Mon Sep 17 00:00:00 2001 From: mannipje <135017126+mannipje@users.noreply.github.com> Date: Fri, 20 Sep 2024 15:14:10 +0000 Subject: [PATCH 05/18] feat(web): Add default header for fjarsysla rikisins (#16099) --- .../components/Organization/Wrapper/OrganizationWrapper.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx b/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx index 0ee977d92b69..438ff2286c17 100644 --- a/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx +++ b/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx @@ -387,7 +387,9 @@ export const OrganizationHeader: React.FC< case 'landing_page': return null case 'fjarsysla-rikisins': - return ( + return n('usingDefaultHeader', false) ? ( + + ) : ( Date: Mon, 23 Sep 2024 08:23:09 +0000 Subject: [PATCH 06/18] fix(register-new-machine): fixing mobile (#16104) --- .../src/fields/AboutMachine/index.tsx | 8 ++++---- .../register-new-machine/src/fields/MachineType/index.tsx | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/libs/application/templates/aosh/register-new-machine/src/fields/AboutMachine/index.tsx b/libs/application/templates/aosh/register-new-machine/src/fields/AboutMachine/index.tsx index 6eee5336c886..b75a10f65199 100644 --- a/libs/application/templates/aosh/register-new-machine/src/fields/AboutMachine/index.tsx +++ b/libs/application/templates/aosh/register-new-machine/src/fields/AboutMachine/index.tsx @@ -122,8 +122,8 @@ export const AboutMachine: FC> = ( return ( - - + + > = ( } /> - + > = ( - + { diff --git a/libs/application/templates/aosh/register-new-machine/src/fields/MachineType/index.tsx b/libs/application/templates/aosh/register-new-machine/src/fields/MachineType/index.tsx index 63d02c50394f..1ecb1eaa1819 100644 --- a/libs/application/templates/aosh/register-new-machine/src/fields/MachineType/index.tsx +++ b/libs/application/templates/aosh/register-new-machine/src/fields/MachineType/index.tsx @@ -193,8 +193,8 @@ export const MachineType: FC> = ( {formatMessage(machine.labels.machineType.inputTitle)} - - + + Date: Mon, 23 Sep 2024 09:17:32 +0000 Subject: [PATCH 07/18] fix(id-card): null check add (#16100) --- .../templates/id-card/src/utils/getChosenApplicant.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/application/templates/id-card/src/utils/getChosenApplicant.ts b/libs/application/templates/id-card/src/utils/getChosenApplicant.ts index b70232c00d37..ed38dc30ce40 100644 --- a/libs/application/templates/id-card/src/utils/getChosenApplicant.ts +++ b/libs/application/templates/id-card/src/utils/getChosenApplicant.ts @@ -25,7 +25,8 @@ export const getChosenApplicant = ( [], ) as Array - if (applicantIdentity?.nationalId === nationalId) { + //this nationalId null check is only because conditions are rendered before applicant has been chosen + if (!nationalId || applicantIdentity?.nationalId === nationalId) { return { name: applicantIdentity?.fullName, isApplicant: true, From ff9e6fca8ee6909971e39d23d1b4cd527346d745 Mon Sep 17 00:00:00 2001 From: helgifr Date: Mon, 23 Sep 2024 10:18:33 +0000 Subject: [PATCH 08/18] fix(parental-leave): Parental leave slider now gets correct value in onChangeEnd function (#16105) Co-authored-by: hfhelgason Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../src/components/Slider/Slider.tsx | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/libs/application/ui-components/src/components/Slider/Slider.tsx b/libs/application/ui-components/src/components/Slider/Slider.tsx index 839dab5a47f1..42df0dc0e209 100644 --- a/libs/application/ui-components/src/components/Slider/Slider.tsx +++ b/libs/application/ui-components/src/components/Slider/Slider.tsx @@ -109,10 +109,19 @@ const Slider = ({ const convertDeltaToIndex = (delta: number) => { const currentX = x + delta - - dragX.current = Math.max(min * sizePerCell, Math.min(size.width, currentX)) - - return roundByNum(dragX.current / sizePerCell, step) + const roundedMin = toFixedNumber(min, 1, 10) + dragX.current = Math.max(0, Math.min(size.width, currentX)) + // Get value to display in slider. + // Get max if more or equal to max, get min if less or equal to min and then show rest with only one decimal point. + const index = + dragX.current / sizePerCell + min >= max + ? max + : dragX.current / sizePerCell + min <= min + ? min + : roundByNum(dragX.current / sizePerCell, step) === 0 + ? min + : roundByNum(dragX.current / sizePerCell, step) + roundedMin + return index } useEffect(() => { @@ -146,19 +155,7 @@ const Slider = ({ const dragBind = useDrag({ onDragMove(deltaX: number) { - const currentX = x + deltaX - const roundedMin = toFixedNumber(min, 1, 10) - dragX.current = Math.max(0, Math.min(size.width, currentX)) - // Get value to display in slider. - // Get max if more or equal to max, get min if less or equal to min and then show rest with only one decimal point. - const index = - dragX.current / sizePerCell + min >= max - ? max - : dragX.current / sizePerCell + min <= min - ? min - : roundByNum(dragX.current / sizePerCell, step) === 0 - ? min - : roundByNum(dragX.current / sizePerCell, step) + roundedMin + const index = convertDeltaToIndex(deltaX) if (onChange && index !== indexRef.current) { onChange(index) @@ -183,7 +180,6 @@ const Slider = ({ dragX.current = undefined if (onChangeEnd) { const index = convertDeltaToIndex(deltaX) - onChangeEnd?.(index) } setIsDragging(false) From c5b064acd3b42cd1fe0cad85d05ae29a6509da89 Mon Sep 17 00:00:00 2001 From: mannipje <135017126+mannipje@users.noreply.github.com> Date: Mon, 23 Sep 2024 10:29:13 +0000 Subject: [PATCH 09/18] feat(web): Add default header for HSU organization (#16107) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../Organization/Wrapper/OrganizationWrapper.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx b/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx index 438ff2286c17..85bae312c8ee 100644 --- a/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx +++ b/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx @@ -333,7 +333,15 @@ export const OrganizationHeader: React.FC< /> ) case 'hsu': - return ( + return n('usingDefaultHeader', false) ? ( + + ) : ( Date: Mon, 23 Sep 2024 10:53:24 +0000 Subject: [PATCH 10/18] fix(hid-application): Remove old condition (#16109) --- .../src/forms/HealthInsuranceDeclarationForm.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/libs/application/templates/health-insurance-declaration/src/forms/HealthInsuranceDeclarationForm.ts b/libs/application/templates/health-insurance-declaration/src/forms/HealthInsuranceDeclarationForm.ts index ed46fa830f95..801ff456b246 100644 --- a/libs/application/templates/health-insurance-declaration/src/forms/HealthInsuranceDeclarationForm.ts +++ b/libs/application/templates/health-insurance-declaration/src/forms/HealthInsuranceDeclarationForm.ts @@ -302,8 +302,6 @@ export const HealthInsuranceDeclarationForm: Form = buildForm({ ], }), ], - condition: (answers: FormValue) => - !!(answers.hasSpouse || answers.hasChildren), }), buildSection({ id: 'residencySectionTourist', From f4ab138ccb1127b9f1b001efbd937d27bb34c219 Mon Sep 17 00:00:00 2001 From: albinagu <47886428+albinagu@users.noreply.github.com> Date: Mon, 23 Sep 2024 11:03:32 +0000 Subject: [PATCH 11/18] fix(service-portal): signature collection paper signees tweak (#16108) * fix(service-portal): signature collection paper signees tweak * module name tweak sp --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- libs/service-portal/signature-collection/src/module.tsx | 2 +- .../Parliamentary/OwnerView/ViewList/Signees/PaperSignees.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/service-portal/signature-collection/src/module.tsx b/libs/service-portal/signature-collection/src/module.tsx index 50bc7c53217f..0474999b3b3d 100644 --- a/libs/service-portal/signature-collection/src/module.tsx +++ b/libs/service-portal/signature-collection/src/module.tsx @@ -37,7 +37,7 @@ export const signatureCollectionModule: PortalModule = { element: , }, { - name: m.signatureCollectionPresidentialLists, + name: m.signatureCollectionParliamentaryLists, path: SignatureCollectionPaths.ViewParliamentaryList, enabled: userInfo.scopes.includes(ApiScope.signatureCollection), key: 'ParliamentaryLists', diff --git a/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees/PaperSignees.tsx b/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees/PaperSignees.tsx index f101e7bd3d80..877cf5b5bd8f 100644 --- a/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees/PaperSignees.tsx +++ b/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees/PaperSignees.tsx @@ -126,7 +126,7 @@ export const PaperSignees = ({ }} error={nationalIdTypo ? ' ' : undefined} loading={loading || loadingCanSign} - icon={canSign ? 'checkmark' : undefined} + icon={name && canSign ? 'checkmark' : undefined} /> @@ -156,7 +156,7 @@ export const PaperSignees = ({ + ) + + return ( + + navigate(-1)} /> +
+ +
+ + +
+ {fromIdentity?.name && ( + + )} + + + + + {fromIdentityQueryLoading ? ( + + ) : fromIdentity?.name ? ( + { + setFromNationalId('') + setFromIdentity(null) + if (defaultFromNationalId) { + setSearchParams( + (params) => { + params.delete('fromNationalId') + return params + }, + { replace: true }, + ) + } + + setTimeout(() => { + if (fromInputRef.current) { + fromInputRef.current.focus() + } + }, 0) + }} + /> + ) : null} +
+
+ +
+ {toIdentity?.name && ( + + )} + + + + {toIdentityQueryLoading ? ( + + ) : toIdentity?.name ? ( + { + setToNationalId('') + setToIdentity(null) + setTimeout(() => { + if (toInputRef.current) { + toInputRef.current.focus() + } + }, 0) + }} + /> + ) : null} +
+
+ + + + )} + + + + + + navigate(DelegationAdminPaths.Root)} + divider={false} + confirmLabel={formatMessage(m.create)} + showShadow={showShadow} + confirmIcon="arrowForward" + /> + + {actionData?.globalError && ( + + + + )} +
+
+
+ + { + setIsConfirmed(false) + setShowConfirmModal(false) + }} + onConfirm={() => { + submit(formRef.current) + setShowConfirmModal(false) + }} + /> +
+ ) +} + +export default CreateDelegationScreen diff --git a/libs/portals/admin/delegation-admin/src/screens/DelegationAdminDetails/DelegationAdmin.tsx b/libs/portals/admin/delegation-admin/src/screens/DelegationAdminDetails/DelegationAdmin.tsx index 2bfcabf4fece..257ee945faa1 100644 --- a/libs/portals/admin/delegation-admin/src/screens/DelegationAdminDetails/DelegationAdmin.tsx +++ b/libs/portals/admin/delegation-admin/src/screens/DelegationAdminDetails/DelegationAdmin.tsx @@ -1,30 +1,71 @@ -import { Box, Stack, Tabs } from '@island.is/island-ui/core' +import { + Button, + GridColumn, + GridRow, + Stack, + Tabs, +} from '@island.is/island-ui/core' import { BackButton } from '@island.is/portals/admin/core' import { useLocale } from '@island.is/localization' import { useLoaderData, useNavigate } from 'react-router-dom' import { DelegationAdminResult } from './DelegationAdmin.loader' import { DelegationAdminPaths } from '../../lib/paths' -import { IntroHeader } from '@island.is/portals/core' +import { formatNationalId, IntroHeader } from '@island.is/portals/core' import { m } from '../../lib/messages' import React from 'react' import DelegationList from '../../components/DelegationList' import { AuthCustomDelegation } from '@island.is/api/schema' import { DelegationsEmptyState } from '@island.is/portals/shared-modules/delegations' +import { useAuth } from '@island.is/auth/react' +import { AdminPortalScope } from '@island.is/auth/scopes' +import { maskString } from '@island.is/shared/utils' const DelegationAdminScreen = () => { const { formatMessage } = useLocale() const navigate = useNavigate() const delegationAdmin = useLoaderData() as DelegationAdminResult + const { userInfo } = useAuth() + + const hasAdminAccess = userInfo?.scopes.includes( + AdminPortalScope.delegationSystemAdmin, + ) return ( navigate(DelegationAdminPaths.Root)} /> - - - + + + + + {hasAdminAccess && ( + + + + )} + { const [focused, setFocused] = useState(false) @@ -14,6 +22,12 @@ const Root = () => { const { formatMessage } = useLocale() const { isSubmitting, isLoading } = useSubmitting() const [error, setError] = useState({ hasError: false, message: '' }) + const navigate = useNavigate() + const { userInfo } = useAuth() + + const hasAdminAccess = userInfo?.scopes.includes( + AdminPortalScope.delegationSystemAdmin, + ) useEffect(() => { if (actionData?.errors) { @@ -31,39 +45,52 @@ const Root = () => { const onFocus = () => setFocused(true) const onBlur = () => setFocused(false) + return ( <> - - - -
- - setSearchInput(event.target.value), - onBlur, - onFocus, - placeholder: formatMessage(formatMessage(m.search)), - colored: true, - }} - buttonProps={{ - type: 'submit', - disabled: searchInput.length === 0, - }} - hasError={error.hasError} - errorMessage={error.hasError ? error.message : undefined} + + + - -
- +
+ {hasAdminAccess && ( + + + + )} + +
+ setSearchInput(event.target.value), + onBlur, + onFocus, + placeholder: formatMessage(formatMessage(m.search)), + colored: true, + }} + buttonProps={{ + type: 'submit', + disabled: searchInput.length === 0, + }} + hasError={error.hasError} + errorMessage={error.hasError ? error.message : undefined} + /> + +
+
) diff --git a/libs/portals/shared-modules/delegations/src/components/delegations/DelegationsFormFooter.tsx b/libs/portals/shared-modules/delegations/src/components/delegations/DelegationsFormFooter.tsx index 2208eb5cf368..7be4aa190ccc 100644 --- a/libs/portals/shared-modules/delegations/src/components/delegations/DelegationsFormFooter.tsx +++ b/libs/portals/shared-modules/delegations/src/components/delegations/DelegationsFormFooter.tsx @@ -20,6 +20,7 @@ type DelegationsFormFooterProps = { confirmIcon?: IconType confirmButtonColorScheme?: 'destructive' | 'default' showShadow?: boolean + divider?: boolean containerPaddingBottom?: ResponsiveSpace } @@ -31,6 +32,7 @@ export const DelegationsFormFooter = ({ confirmIcon, confirmButtonColorScheme = 'default', showShadow = true, + divider = true, containerPaddingBottom = 4, ...rest }: DelegationsFormFooterProps) => { @@ -39,9 +41,11 @@ export const DelegationsFormFooter = ({ return (
-
- -
+ {divider && ( +
+ +
+ )} Date: Mon, 23 Sep 2024 15:20:28 +0000 Subject: [PATCH 16/18] fix(native-app): use correct locale in applications query (#16115) * fix: use correct locale in applications query * fix: add margin for empty state in applications module * fix: show progress bar for applications with 0 of x steps finished --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../src/screens/applications/applications.tsx | 4 +- .../components/applications-preview.tsx | 2 +- .../src/screens/home/applications-module.tsx | 39 ++++++++++++------- .../app/src/ui/lib/card/status-card.tsx | 6 ++- 4 files changed, 32 insertions(+), 19 deletions(-) diff --git a/apps/native/app/src/screens/applications/applications.tsx b/apps/native/app/src/screens/applications/applications.tsx index 33b459f40811..42e157caf749 100644 --- a/apps/native/app/src/screens/applications/applications.tsx +++ b/apps/native/app/src/screens/applications/applications.tsx @@ -104,8 +104,10 @@ export const ApplicationsScreen: NavigationFunctionComponent = ({ const [hiddenContent, setHiddenContent] = useState(isIos) const { locale } = usePreferencesStore() + const queryLocale = locale === 'is-IS' ? 'is' : 'en' + const applicationsRes = useListApplicationsQuery({ - variables: { locale: locale === 'is-US' ? 'is' : 'en' }, + variables: { locale: queryLocale }, }) const applications = useMemo( diff --git a/apps/native/app/src/screens/applications/components/applications-preview.tsx b/apps/native/app/src/screens/applications/components/applications-preview.tsx index cec7b6fd35c5..38d3948e7ca3 100644 --- a/apps/native/app/src/screens/applications/components/applications-preview.tsx +++ b/apps/native/app/src/screens/applications/components/applications-preview.tsx @@ -83,7 +83,7 @@ export const ApplicationsPreview = ({ }, )} progressContainerWidth={ - slider ? screenWidth - theme.spacing[2] * 6 : undefined + slider && count > 1 ? screenWidth - theme.spacing[2] * 6 : undefined } description={ type !== 'incomplete' diff --git a/apps/native/app/src/screens/home/applications-module.tsx b/apps/native/app/src/screens/home/applications-module.tsx index 076b53ec768e..0b0210b7cdae 100644 --- a/apps/native/app/src/screens/home/applications-module.tsx +++ b/apps/native/app/src/screens/home/applications-module.tsx @@ -1,7 +1,8 @@ import { EmptyCard, StatusCardSkeleton } from '@ui' import React from 'react' import { useIntl } from 'react-intl' -import { Image, SafeAreaView } from 'react-native' +import styled from 'styled-components' +import { Image, SafeAreaView, View } from 'react-native' import { ApolloError } from '@apollo/client' import leJobss3 from '../../assets/illustrations/le-jobs-s3.png' @@ -20,6 +21,10 @@ interface ApplicationsModuleProps { componentId: string } +const Wrapper = styled(View)` + margin-horizontal: ${({ theme }) => theme.spacing[2]}px; +` + const validateApplicationsInitialData = ({ data, loading, @@ -50,23 +55,27 @@ const ApplicationsModule = React.memo( return ( {loading && !data ? ( - + + + ) : ( <> {count === 0 && ( - - } - link={null} - /> + + + } + link={null} + /> + )} {count !== 0 && ( @@ -128,9 +130,9 @@ export function StatusCard({ {!!description && {description}} - {!!progress && ( + {!hideProgress && ( Date: Mon, 23 Sep 2024 21:20:10 +0000 Subject: [PATCH 17/18] feat(ids-admin): Delegation-Delegation-Type (#16068) * created delegation-delegation-type.model.ts and updated findAllScopesTo in delegation-scope.service.ts * fix broken tests * tests for findAllScopesTo * added validTo to delegationDelegationType * set general mandate as type in ids select account prompt * Get general mandate to delegations-to on service-portal * remove duplicate case * small refactor * chore: nx format:write update dirty files * fix tests after merge with main * move general mandate tests to new file * add zendesk validation * fix comments from PR * fix pr comment * chore: nx format:write update dirty files * fix pr comment --------- Co-authored-by: andes-it Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- ...ersonal-representative.controller.spec.ts} | 5 +- .../test/delegations.controller.spec.ts | 278 ++++++++++++++++++ .../actorDelegations.controller.spec.ts | 93 +++++- apps/services/auth/public-api/test/setup.ts | 1 + ...240916092301-delegation-delegation-type.js | 55 ++++ .../seeders/local/012-general-mandate.js | 44 +++ libs/auth-api-lib/src/index.ts | 1 + .../admin/delegation-admin-custom.service.ts | 3 +- .../delegations/delegation-scope.service.ts | 67 ++++- .../delegations-incoming-custom.service.ts | 120 +++++++- .../delegations-incoming.service.ts | 18 +- .../src/lib/delegations/delegations.module.ts | 2 + .../delegation-delegation-type.model.ts | 67 +++++ .../models/delegation-type.model.ts | 4 + .../delegations/models/delegation.model.ts | 14 +- .../admin/delegation-admin/project.json | 6 + .../src/components/access/AccessCard.tsx | 3 + .../delegations/src/lib/messages.ts | 4 + .../src/fixtures/delegation.fixture.ts | 2 +- 19 files changed, 762 insertions(+), 25 deletions(-) rename apps/services/auth/ids-api/src/app/delegations/{delegations.controller.spec.ts => delegations-personal-representative.controller.spec.ts} (99%) create mode 100644 apps/services/auth/ids-api/src/app/delegations/test/delegations.controller.spec.ts create mode 100644 libs/auth-api-lib/migrations/20240916092301-delegation-delegation-type.js create mode 100644 libs/auth-api-lib/seeders/local/012-general-mandate.js create mode 100644 libs/auth-api-lib/src/lib/delegations/models/delegation-delegation-type.model.ts diff --git a/apps/services/auth/ids-api/src/app/delegations/delegations.controller.spec.ts b/apps/services/auth/ids-api/src/app/delegations/delegations-personal-representative.controller.spec.ts similarity index 99% rename from apps/services/auth/ids-api/src/app/delegations/delegations.controller.spec.ts rename to apps/services/auth/ids-api/src/app/delegations/delegations-personal-representative.controller.spec.ts index e5f5875ce638..f0050b0d9e9e 100644 --- a/apps/services/auth/ids-api/src/app/delegations/delegations.controller.spec.ts +++ b/apps/services/auth/ids-api/src/app/delegations/delegations-personal-representative.controller.spec.ts @@ -6,6 +6,7 @@ import request from 'supertest' import { ApiScope, ApiScopeDelegationType, + Client, DelegationProviderModel, DelegationsIndexService, DelegationTypeModel, @@ -52,12 +53,13 @@ import { personalRepresentativeType, } from '../../../test/stubs/personalRepresentativeStubs' -describe('DelegationsController', () => { +describe('Personal Representative DelegationsController', () => { describe('Given a user is authenticated', () => { let app: TestApp let factory: FixtureFactory let server: request.SuperTest let apiScopeModel: typeof ApiScope + let clientModel: typeof Client let prScopePermission: typeof PersonalRepresentativeScopePermission let apiScopeDelegationTypeModel: typeof ApiScopeDelegationType let prModel: typeof PersonalRepresentative @@ -143,6 +145,7 @@ describe('DelegationsController', () => { delegationProviderModel = app.get( getModelToken(DelegationProviderModel), ) + clientModel = app.get(getModelToken(Client)) nationalRegistryApi = app.get(NationalRegistryClientService) delegationIndexService = app.get(DelegationsIndexService) factory = new FixtureFactory(app) diff --git a/apps/services/auth/ids-api/src/app/delegations/test/delegations.controller.spec.ts b/apps/services/auth/ids-api/src/app/delegations/test/delegations.controller.spec.ts new file mode 100644 index 000000000000..4abd4987bd31 --- /dev/null +++ b/apps/services/auth/ids-api/src/app/delegations/test/delegations.controller.spec.ts @@ -0,0 +1,278 @@ +import { getModelToken } from '@nestjs/sequelize' +import request from 'supertest' +import { uuid } from 'uuidv4' +import addDays from 'date-fns/addDays' + +import { + ApiScope, + ApiScopeDelegationType, + Delegation, + DelegationDelegationType, + DelegationProviderModel, + DelegationScope, + DelegationTypeModel, + Domain, +} from '@island.is/auth-api-lib' +import { NationalRegistryClientService } from '@island.is/clients/national-registry-v2' +import { + createClient, + createDomain, + FixtureFactory, +} from '@island.is/services/auth/testing' +import { + AuthDelegationProvider, + AuthDelegationType, +} from '@island.is/shared/types' +import { + createCurrentUser, + createNationalRegistryUser, +} from '@island.is/testing/fixtures' +import { TestApp } from '@island.is/testing/nest' + +import { defaultScopes, setupWithAuth } from '../../../../test/setup' +import { getFakeNationalId } from '../../../../test/stubs/genericStubs' + +describe('DelegationsController', () => { + describe('Given a user is authenticated', () => { + let app: TestApp + let factory: FixtureFactory + let server: request.SuperTest + + let apiScopeModel: typeof ApiScope + let apiScopeDelegationTypeModel: typeof ApiScopeDelegationType + let delegationDelegationTypeModel: typeof DelegationDelegationType + let delegationModel: typeof Delegation + let delegationTypeModel: typeof DelegationTypeModel + let nationalRegistryApi: NationalRegistryClientService + let delegationProviderModel: typeof DelegationProviderModel + let delegationScopesModel: typeof DelegationScope + + const client = createClient({ + clientId: '@island.is/webapp', + }) + + const scopeValid1 = 'scope/valid1' + const scopeValid2 = 'scope/valid2' + const scopeValid1and2 = 'scope/valid1and2' + const scopeUnactiveType = 'scope/unactiveType' + const scopeOutdated = 'scope/outdated' + const disabledScope = 'disabledScope' + + client.allowedScopes = Object.values([ + scopeValid1, + scopeValid2, + scopeValid1and2, + scopeUnactiveType, + scopeOutdated, + disabledScope, + ]).map((s) => ({ + clientId: client.clientId, + scopeName: s, + })) + + const userNationalId = getFakeNationalId() + + const user = createCurrentUser({ + nationalId: userNationalId, + scope: [defaultScopes.testUserHasAccess.name], + client: client.clientId, + }) + + const domain = createDomain() + + beforeAll(async () => { + app = await setupWithAuth({ + user, + }) + server = request(app.getHttpServer()) + + const domainModel = app.get(getModelToken(Domain)) + await domainModel.create(domain) + + apiScopeModel = app.get(getModelToken(ApiScope)) + + apiScopeDelegationTypeModel = app.get( + getModelToken(ApiScopeDelegationType), + ) + delegationTypeModel = app.get( + getModelToken(DelegationTypeModel), + ) + delegationProviderModel = app.get( + getModelToken(DelegationProviderModel), + ) + delegationScopesModel = app.get( + getModelToken(DelegationScope), + ) + delegationModel = app.get(getModelToken(Delegation)) + delegationDelegationTypeModel = app.get( + getModelToken(DelegationDelegationType), + ) + nationalRegistryApi = app.get(NationalRegistryClientService) + factory = new FixtureFactory(app) + }) + + afterAll(async () => { + await app.cleanUp() + }) + + describe('GET with general mandate delegation type', () => { + const representeeNationalId = getFakeNationalId() + let nationalRegistryApiSpy: jest.SpyInstance + const scopeNames = [ + 'api-scope/generalMandate1', + 'api-scope/generalMandate2', + 'api-scope/generalMandate3', + ] + + beforeAll(async () => { + client.supportedDelegationTypes = [ + AuthDelegationType.GeneralMandate, + AuthDelegationType.LegalGuardian, + ] + await factory.createClient(client) + + const delegations = await delegationModel.create({ + id: uuid(), + fromDisplayName: 'Test', + fromNationalId: representeeNationalId, + toNationalId: userNationalId, + toName: 'Test', + }) + + await delegationProviderModel.create({ + id: AuthDelegationProvider.Custom, + name: 'Custom', + description: 'Custom', + }) + + await delegationDelegationTypeModel.create({ + delegationId: delegations.id, + delegationTypeId: AuthDelegationType.GeneralMandate, + }) + + await apiScopeModel.bulkCreate( + scopeNames.map((name) => ({ + name, + domainName: domain.name, + enabled: true, + description: `${name}: description`, + displayName: `${name}: display name`, + })), + ) + + // set 2 of 3 scopes to have general mandate delegation type + await apiScopeDelegationTypeModel.bulkCreate([ + { + apiScopeName: scopeNames[0], + delegationType: AuthDelegationType.GeneralMandate, + }, + { + apiScopeName: scopeNames[1], + delegationType: AuthDelegationType.GeneralMandate, + }, + ]) + + nationalRegistryApiSpy = jest + .spyOn(nationalRegistryApi, 'getIndividual') + .mockImplementation(async (id) => { + const user = createNationalRegistryUser({ + nationalId: representeeNationalId, + }) + + return user ?? null + }) + }) + + afterAll(async () => { + await app.cleanUp() + nationalRegistryApiSpy.mockClear() + }) + + it('should return mergedDelegationDTO with the generalMandate', async () => { + const response = await server.get('/v2/delegations') + + expect(response.status).toEqual(200) + expect(response.body).toHaveLength(1) + }) + + it('should get all general mandate scopes', async () => { + const response = await server.get('/delegations/scopes').query({ + fromNationalId: representeeNationalId, + delegationType: [AuthDelegationType.GeneralMandate], + }) + + expect(response.status).toEqual(200) + expect(response.body).toEqual([scopeNames[0], scopeNames[1]]) + }) + + it('should only return valid general mandates', async () => { + const newNationalId = getFakeNationalId() + const newDelegation = await delegationModel.create({ + id: uuid(), + fromDisplayName: 'Test', + fromNationalId: newNationalId, + toNationalId: userNationalId, + toName: 'Test', + }) + + await delegationDelegationTypeModel.create({ + delegationId: newDelegation.id, + delegationTypeId: AuthDelegationType.GeneralMandate, + validTo: addDays(new Date(), -2), + }) + + const response = await server.get('/delegations/scopes').query({ + fromNationalId: newNationalId, + delegationType: [AuthDelegationType.GeneralMandate], + }) + + expect(response.status).toEqual(200) + expect(response.body).toEqual([]) + }) + + it('should return all general mandate scopes and other preset scopes', async () => { + const newDelegation = await delegationModel.create({ + id: uuid(), + fromDisplayName: 'Test', + fromNationalId: representeeNationalId, + domainName: domain.name, + toNationalId: userNationalId, + toName: 'Test', + }) + + await delegationTypeModel.create({ + id: AuthDelegationType.Custom, + name: 'custom', + description: 'custom', + providerId: AuthDelegationProvider.Custom, + }) + + await delegationScopesModel.create({ + id: uuid(), + delegationId: newDelegation.id, + scopeName: scopeNames[2], + // set valid from as yesterday and valid to as tomorrow + validFrom: addDays(new Date(), -1), + validTo: addDays(new Date(), 1), + }) + + await apiScopeDelegationTypeModel.create({ + apiScopeName: scopeNames[2], + delegationType: AuthDelegationType.LegalGuardian, + }) + + const response = await server.get('/delegations/scopes').query({ + fromNationalId: representeeNationalId, + delegationType: [ + AuthDelegationType.GeneralMandate, + AuthDelegationType.LegalGuardian, + ], + }) + + expect(response.status).toEqual(200) + expect(response.body).toEqual(expect.arrayContaining(scopeNames)) + expect(response.body).toHaveLength(scopeNames.length) + }) + }) + }) +}) diff --git a/apps/services/auth/public-api/src/app/modules/delegations/actorDelegations.controller.spec.ts b/apps/services/auth/public-api/src/app/modules/delegations/actorDelegations.controller.spec.ts index c4183a2a61d7..b178739555d5 100644 --- a/apps/services/auth/public-api/src/app/modules/delegations/actorDelegations.controller.spec.ts +++ b/apps/services/auth/public-api/src/app/modules/delegations/actorDelegations.controller.spec.ts @@ -3,10 +3,9 @@ import times from 'lodash/times' import request from 'supertest' import { - ApiScope, - Client, ClientDelegationType, Delegation, + DelegationDelegationType, DelegationDTO, DelegationDTOMapper, DelegationProviderModel, @@ -153,6 +152,7 @@ describe('ActorDelegationsController', () => { let app: TestApp let server: request.SuperTest let delegationModel: typeof Delegation + let delegationDelegationTypeModel: typeof DelegationDelegationType let clientDelegationTypeModel: typeof ClientDelegationType let nationalRegistryApi: NationalRegistryClientService @@ -174,6 +174,9 @@ describe('ActorDelegationsController', () => { clientDelegationTypeModel = app.get( getModelToken(ClientDelegationType), ) + delegationDelegationTypeModel = app.get( + getModelToken(DelegationDelegationType), + ) nationalRegistryApi = app.get(NationalRegistryClientService) }) @@ -293,20 +296,96 @@ describe('ActorDelegationsController', () => { ) }) - it('should return custom delegations when the delegationTypes filter has custom type', async () => { + it('should return custom delegations and general mandate when the delegationTypes filter has both types and delegation exists for both', async () => { // Arrange + const delegation = createDelegation({ + fromNationalId: nationalRegistryUser.nationalId, + toNationalId: user.nationalId, + scopes: [], + }) + + await delegationModel.create(delegation) + + await delegationDelegationTypeModel.create({ + delegationId: delegation.id, + delegationTypeId: AuthDelegationType.GeneralMandate, + }) + await createDelegationModels(delegationModel, [ mockDelegations.incomingWithOtherDomain, ]) + + // Act + const res = await server.get( + `${path}${query}&delegationTypes=${AuthDelegationType.Custom}&delegationTypes=${AuthDelegationType.GeneralMandate}`, + ) + + // Assert + expect(res.status).toEqual(200) + expect(res.body).toHaveLength(2) + expect( + res.body + .map((d: MergedDelegationDTO) => d.types) + .flat() + .sort(), + ).toEqual( + [AuthDelegationType.Custom, AuthDelegationType.GeneralMandate].sort(), + ) + }) + + it('should return a merged object with both Custom and GeneralMandate types', async () => { + // Arrange + const delegation = createDelegation({ + fromNationalId: + mockDelegations.incomingWithOtherDomain.fromNationalId, + toNationalId: user.nationalId, + domainName: null, + scopes: [], + }) + + await delegationModel.create(delegation) + + await delegationDelegationTypeModel.create({ + delegationId: delegation.id, + delegationTypeId: AuthDelegationType.GeneralMandate, + }) + + await createDelegationModels(delegationModel, [ + mockDelegations.incomingWithOtherDomain, + ]) + + // Act + const res = await server.get( + `${path}${query}&delegationTypes=${AuthDelegationType.Custom}&delegationTypes=${AuthDelegationType.GeneralMandate}`, + ) + + // Assert + expect(res.status).toEqual(200) + expect(res.body).toHaveLength(1) + expect( + res.body + .map((d: MergedDelegationDTO) => d.types) + .flat() + .sort(), + ).toEqual( + [AuthDelegationType.Custom, AuthDelegationType.GeneralMandate].sort(), + ) + }) + + it('should return only delegations related to the provided otherUser national id', async () => { + // Arrange + await createDelegationModels(delegationModel, [ + mockDelegations.incoming, + ]) const expectedModel = await findExpectedMergedDelegationModels( delegationModel, - mockDelegations.incomingWithOtherDomain.id, + mockDelegations.incoming.id, [Scopes[0].name], ) // Act const res = await server.get( - `${path}${query}&delegationTypes=${AuthDelegationType.Custom}`, + `${path}${query}&delegationTypes=${AuthDelegationType.Custom}&otherUser=${mockDelegations.incoming.fromNationalId}`, ) // Assert @@ -321,7 +400,7 @@ describe('ActorDelegationsController', () => { ) }) - it('should return only delegations related to the provided otherUser national id', async () => { + it('should return only delegations related to the provided otherUser national id without the general mandate since there is none', async () => { // Arrange await createDelegationModels(delegationModel, [ mockDelegations.incoming, @@ -334,7 +413,7 @@ describe('ActorDelegationsController', () => { // Act const res = await server.get( - `${path}${query}&delegationTypes=${AuthDelegationType.Custom}&otherUser=${mockDelegations.incoming.fromNationalId}`, + `${path}${query}&delegationTypes=${AuthDelegationType.Custom}&delegationTypes${AuthDelegationType.GeneralMandate}&otherUser=${mockDelegations.incoming.fromNationalId}`, ) // Assert diff --git a/apps/services/auth/public-api/test/setup.ts b/apps/services/auth/public-api/test/setup.ts index c56ac554a07d..5cd632add54c 100644 --- a/apps/services/auth/public-api/test/setup.ts +++ b/apps/services/auth/public-api/test/setup.ts @@ -72,6 +72,7 @@ export const delegationTypes = [ AuthDelegationType.LegalGuardian, AuthDelegationType.ProcurationHolder, AuthDelegationType.PersonalRepresentative, + AuthDelegationType.GeneralMandate, ] export const ScopeGroups: ScopeGroupSetupOptions[] = [ diff --git a/libs/auth-api-lib/migrations/20240916092301-delegation-delegation-type.js b/libs/auth-api-lib/migrations/20240916092301-delegation-delegation-type.js new file mode 100644 index 000000000000..453370fae9c1 --- /dev/null +++ b/libs/auth-api-lib/migrations/20240916092301-delegation-delegation-type.js @@ -0,0 +1,55 @@ +'use strict' + +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.createTable('delegation_delegation_type', { + delegation_id: { + type: Sequelize.UUID, + allowNull: false, + references: { + model: 'delegation', // Table name + key: 'id', + }, + onDelete: 'CASCADE', + primaryKey: true, + }, + delegation_type_id: { + type: Sequelize.STRING, + allowNull: false, + references: { + model: 'delegation_type', // Table name + key: 'id', + }, + onDelete: 'CASCADE', + primaryKey: true, + }, + valid_to: { + type: Sequelize.DATE, + allowNull: true, + }, + created: { + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.fn('now'), + }, + modified: { + type: Sequelize.DATE, + }, + }) + + await queryInterface.addConstraint('delegation_delegation_type', { + fields: ['delegation_id', 'delegation_type_id'], + type: 'unique', + name: 'unique_delegation_delegation_type', + }) + }, + + async down(queryInterface) { + await queryInterface.removeConstraint( + 'delegation_delegation_type', + 'unique_delegation_delegation_type', + ) + + await queryInterface.dropTable('delegation_delegation_type') + }, +} diff --git a/libs/auth-api-lib/seeders/local/012-general-mandate.js b/libs/auth-api-lib/seeders/local/012-general-mandate.js new file mode 100644 index 000000000000..7f97c92309fc --- /dev/null +++ b/libs/auth-api-lib/seeders/local/012-general-mandate.js @@ -0,0 +1,44 @@ +const { uuid } = require('uuidv4') + +module.exports = { + up: async (queryInterface) => { + const transaction = await queryInterface.sequelize.transaction() + const id = uuid() + + try { + await queryInterface.bulkInsert( + 'delegation', + [ + { + id, + to_national_id: '0101302399', + from_national_id: '0101307789', + from_display_name: 'Gervimaður útlönd', + to_name: 'Gervimaður Færeyjar', + }, + ], + { transaction }, + ) + + await queryInterface.bulkInsert( + 'delegation_delegation_type', + [ + { + delegation_id: id, + delegation_type_id: 'GeneralMandate', + }, + ], + { transaction }, + ) + + await transaction.commit() + } catch (err) { + await transaction.rollback() + console.log(err) + throw err + } + }, + down: async () => { + // Do nothing + }, +} diff --git a/libs/auth-api-lib/src/index.ts b/libs/auth-api-lib/src/index.ts index d39fa234af6f..68842edcc489 100644 --- a/libs/auth-api-lib/src/index.ts +++ b/libs/auth-api-lib/src/index.ts @@ -52,6 +52,7 @@ export * from './lib/delegations/models/delegation-scope.model' export * from './lib/delegations/models/delegation-index.model' export * from './lib/delegations/models/delegation-index-meta.model' export * from './lib/delegations/models/delegation-type.model' +export * from './lib/delegations/models/delegation-delegation-type.model' export * from './lib/delegations/models/delegation-provider.model' export * from './lib/delegations/DelegationConfig' export * from './lib/delegations/utils/scopes' diff --git a/libs/auth-api-lib/src/lib/delegations/admin/delegation-admin-custom.service.ts b/libs/auth-api-lib/src/lib/delegations/admin/delegation-admin-custom.service.ts index 2a55384d2623..f5f878a88e15 100644 --- a/libs/auth-api-lib/src/lib/delegations/admin/delegation-admin-custom.service.ts +++ b/libs/auth-api-lib/src/lib/delegations/admin/delegation-admin-custom.service.ts @@ -97,10 +97,9 @@ export class DelegationAdminCustomService { } async deleteDelegation(user: User, delegationId: string): Promise { - // TODO: Check if delegation has a ReferenceId and throw error if it does not. const delegation = await this.delegationModel.findByPk(delegationId) - if (!delegation) { + if (!delegation || !delegation.referenceId) { throw new NoContentException() } diff --git a/libs/auth-api-lib/src/lib/delegations/delegation-scope.service.ts b/libs/auth-api-lib/src/lib/delegations/delegation-scope.service.ts index bda3d0856fcc..bb6f5a332112 100644 --- a/libs/auth-api-lib/src/lib/delegations/delegation-scope.service.ts +++ b/libs/auth-api-lib/src/lib/delegations/delegation-scope.service.ts @@ -6,7 +6,10 @@ import startOfDay from 'date-fns/startOfDay' import { Op, Transaction } from 'sequelize' import { uuid } from 'uuidv4' -import { AuthDelegationProvider } from '@island.is/shared/types' +import { + AuthDelegationProvider, + AuthDelegationType, +} from '@island.is/shared/types' import { PersonalRepresentativeDelegationTypeModel } from '../personal-representative/models/personal-representative-delegation-type.model' import { PersonalRepresentative } from '../personal-representative/models/personal-representative.model' @@ -21,6 +24,7 @@ import { DelegationTypeModel } from './models/delegation-type.model' import { Delegation } from './models/delegation.model' import type { User } from '@island.is/auth-nest-tools' +import { DelegationDelegationType } from './models/delegation-delegation-type.model' @Injectable() export class DelegationScopeService { @@ -31,6 +35,8 @@ export class DelegationScopeService { private apiScopeModel: typeof ApiScope, @InjectModel(IdentityResource) private identityResourceModel: typeof IdentityResource, + @InjectModel(Delegation) + private delegationModel: typeof Delegation, @Inject(DelegationConfig.KEY) private delegationConfig: ConfigType, private delegationProviderService: DelegationProviderService, @@ -121,7 +127,7 @@ export class DelegationScopeService { toNationalId: string, fromNationalId: string, ): Promise { - const today = startOfDay(new Date()) + const today = new Date() return this.delegationScopeModel.findAll({ where: { @@ -161,6 +167,51 @@ export class DelegationScopeService { }) } + private async findValidGeneralMandateScopesTo( + toNationalId: string, + fromNationalId: string, + ): Promise { + const today = startOfDay(new Date()) + + const delegations = await this.delegationModel.findAll({ + where: { + toNationalId, + fromNationalId, + }, + include: [ + { + model: DelegationDelegationType, + required: true, + where: { + delegationTypeId: AuthDelegationType.GeneralMandate, + validTo: { + [Op.or]: [{ [Op.is]: undefined }, { [Op.gte]: today }], + }, + }, + }, + ], + }) + + if (delegations.length === 0) return [] + + return this.apiScopeModel + .findAll({ + attributes: ['name'], + where: { + enabled: true, + }, + include: [ + { + model: ApiScopeDelegationType, + where: { + delegationType: AuthDelegationType.GeneralMandate, + }, + }, + ], + }) + .then((apiScopes) => apiScopes.map((apiScope) => apiScope.name)) + } + private async findAllNationalRegistryScopes(): Promise { const apiScopes = await this.apiScopeModel.findAll({ include: [ @@ -296,7 +347,7 @@ export class DelegationScopeService { if ( providers.includes(AuthDelegationProvider.PersonalRepresentativeRegistry) - ) + ) { scopePromises.push( this.findPersonalRepresentativeRegistryScopes( user.nationalId, @@ -304,14 +355,22 @@ export class DelegationScopeService { delegationTypes, ), ) + } - if (providers.includes(AuthDelegationProvider.Custom)) + if (delegationTypes?.includes(AuthDelegationType.Custom)) { scopePromises.push( this.findValidCustomScopesTo(user.nationalId, fromNationalId).then( (delegationScopes: DelegationScope[]) => delegationScopes.map((ds) => ds.scopeName), ), ) + } + + if (delegationTypes?.includes(AuthDelegationType.GeneralMandate)) { + scopePromises.push( + this.findValidGeneralMandateScopesTo(user.nationalId, fromNationalId), + ) + } const scopeSets = await Promise.all(scopePromises) diff --git a/libs/auth-api-lib/src/lib/delegations/delegations-incoming-custom.service.ts b/libs/auth-api-lib/src/lib/delegations/delegations-incoming-custom.service.ts index 40022c8082fc..b13ce486becc 100644 --- a/libs/auth-api-lib/src/lib/delegations/delegations-incoming-custom.service.ts +++ b/libs/auth-api-lib/src/lib/delegations/delegations-incoming-custom.service.ts @@ -3,16 +3,14 @@ import { InjectModel } from '@nestjs/sequelize' import * as kennitala from 'kennitala' import uniqBy from 'lodash/uniqBy' import { Op } from 'sequelize' +import startOfDay from 'date-fns/startOfDay' import { User } from '@island.is/auth-nest-tools' import { IndividualDto, NationalRegistryClientService, } from '@island.is/clients/national-registry-v2' -import { - CompanyExtendedInfo, - CompanyRegistryClientService, -} from '@island.is/clients/rsk/company-registry' +import { CompanyRegistryClientService } from '@island.is/clients/rsk/company-registry' import { LOGGER_PROVIDER } from '@island.is/logging' import { AuditService } from '@island.is/nest/audit' import { AuthDelegationType } from '@island.is/shared/types' @@ -30,6 +28,7 @@ import { Delegation } from './models/delegation.model' import { DelegationValidity } from './types/delegationValidity' import { partitionWithIndex } from './utils/partitionWithIndex' import { getScopeValidityWhereClause } from './utils/scopes' +import { DelegationDelegationType } from './models/delegation-delegation-type.model' type FindAllValidIncomingOptions = { nationalId: string @@ -84,6 +83,33 @@ export class DelegationsIncomingCustomService { }) } + async findAllValidGeneralMandate( + { nationalId }: FindAllValidIncomingOptions, + useMaster = false, + ): Promise { + const { delegations, fromNameInfo } = + await this.findAllIncomingGeneralMandates( + { + nationalId, + }, + useMaster, + ) + + return delegations.map((delegation) => { + const delegationDTO = delegation.toDTO(AuthDelegationType.GeneralMandate) + + const person = this.getPersonByNationalId( + fromNameInfo, + delegationDTO.fromNationalId, + ) + + return { + ...delegationDTO, + fromName: person?.name ?? delegationDTO.fromName ?? UNKNOWN_NAME, + } + }) + } + async findAllAvailableIncoming( user: User, clientAllowedApiScopes: ApiScopeInfo[], @@ -151,6 +177,92 @@ export class DelegationsIncomingCustomService { }) } + async findAllAvailableGeneralMandate( + user: User, + clientAllowedApiScopes: ApiScopeInfo[], + requireApiScopes: boolean, + ): Promise { + const customApiScopes = clientAllowedApiScopes.filter((s) => + s.supportedDelegationTypes?.some( + (dt) => dt.delegationType === AuthDelegationType.GeneralMandate, + ), + ) + + if (requireApiScopes && !(customApiScopes && customApiScopes.length > 0)) { + return [] + } + + const { delegations, fromNameInfo } = + await this.findAllIncomingGeneralMandates({ + nationalId: user.nationalId, + }) + + const mergedDelegationDTOs = uniqBy( + delegations.map((d) => + d.toMergedDTO([AuthDelegationType.GeneralMandate]), + ), + 'fromNationalId', + ) + + return mergedDelegationDTOs.map((d) => { + const person = this.getPersonByNationalId(fromNameInfo, d.fromNationalId) + + return { + ...d, + fromName: person?.name ?? d.fromName ?? UNKNOWN_NAME, + } as MergedDelegationDTO + }) + } + + private async findAllIncomingGeneralMandates( + { nationalId }: FindAllValidIncomingOptions, + useMaster = false, + ): Promise<{ delegations: Delegation[]; fromNameInfo: FromNameInfo[] }> { + const startOfToday = startOfDay(new Date()) + + const delegations = await this.delegationModel.findAll({ + useMaster, + where: { + toNationalId: nationalId, + }, + include: [ + { + model: DelegationDelegationType, + where: { + validTo: { + [Op.or]: { + [Op.gte]: startOfToday, + [Op.is]: null, + }, + }, + delegationTypeId: AuthDelegationType.GeneralMandate, + }, + }, + ], + }) + + // Check live status, i.e. dead or alive for delegations + const { aliveDelegations, deceasedDelegations, fromNameInfo } = + await this.getLiveStatusFromDelegations(delegations) + + if (deceasedDelegations.length > 0) { + // Delete all deceased delegations by deleting them and their scopes. + const deletePromises = deceasedDelegations.map((delegation) => + delegation.destroy(), + ) + + await Promise.all(deletePromises) + + this.auditService.audit({ + action: 'deleteDelegationsForMissingPeople', + resources: deceasedDelegations.map(({ id }) => id).filter(isDefined), + system: true, + }) + } + + return { delegations: aliveDelegations, fromNameInfo } + } + private async findAllIncoming( { nationalId, diff --git a/libs/auth-api-lib/src/lib/delegations/delegations-incoming.service.ts b/libs/auth-api-lib/src/lib/delegations/delegations-incoming.service.ts index 3a162241164b..30eb53b694c3 100644 --- a/libs/auth-api-lib/src/lib/delegations/delegations-incoming.service.ts +++ b/libs/auth-api-lib/src/lib/delegations/delegations-incoming.service.ts @@ -102,6 +102,12 @@ export class DelegationsIncomingService { }), ) + delegationPromises.push( + this.delegationsIncomingCustomService.findAllValidGeneralMandate({ + nationalId: user.nationalId, + }), + ) + delegationPromises.push( this.delegationsIncomingRepresentativeService.findAllIncoming({ nationalId: user.nationalId, @@ -172,7 +178,7 @@ export class DelegationsIncomingService { ) } - if (providers.includes(AuthDelegationProvider.Custom)) { + if (types?.includes(AuthDelegationType.Custom)) { delegationPromises.push( this.delegationsIncomingCustomService.findAllAvailableIncoming( user, @@ -182,6 +188,16 @@ export class DelegationsIncomingService { ) } + if (types?.includes(AuthDelegationType.GeneralMandate)) { + delegationPromises.push( + this.delegationsIncomingCustomService.findAllAvailableGeneralMandate( + user, + clientAllowedApiScopes, + client.requireApiScopes, + ), + ) + } + if ( providers.includes(AuthDelegationProvider.PersonalRepresentativeRegistry) ) { diff --git a/libs/auth-api-lib/src/lib/delegations/delegations.module.ts b/libs/auth-api-lib/src/lib/delegations/delegations.module.ts index 6353fdd7d211..c74aaca8a940 100644 --- a/libs/auth-api-lib/src/lib/delegations/delegations.module.ts +++ b/libs/auth-api-lib/src/lib/delegations/delegations.module.ts @@ -34,6 +34,7 @@ import { DelegationProviderModel } from './models/delegation-provider.model' import { DelegationProviderService } from './delegation-provider.service' import { ApiScopeDelegationType } from '../resources/models/api-scope-delegation-type.model' import { DelegationAdminCustomService } from './admin/delegation-admin-custom.service' +import { DelegationDelegationType } from './models/delegation-delegation-type.model' @Module({ imports: [ @@ -57,6 +58,7 @@ import { DelegationAdminCustomService } from './admin/delegation-admin-custom.se ApiScopeUserAccess, DelegationTypeModel, DelegationProviderModel, + DelegationDelegationType, ]), UserSystemNotificationModule, ], diff --git a/libs/auth-api-lib/src/lib/delegations/models/delegation-delegation-type.model.ts b/libs/auth-api-lib/src/lib/delegations/models/delegation-delegation-type.model.ts new file mode 100644 index 000000000000..ba00c7fc4b3f --- /dev/null +++ b/libs/auth-api-lib/src/lib/delegations/models/delegation-delegation-type.model.ts @@ -0,0 +1,67 @@ +import type { + CreationOptional, + InferAttributes, + InferCreationAttributes, +} from 'sequelize' +import { + Table, + Column, + Model, + ForeignKey, + DataType, + CreatedAt, + UpdatedAt, + BelongsTo, +} from 'sequelize-typescript' + +import { Delegation } from './delegation.model' +import { DelegationTypeModel } from './delegation-type.model' + +@Table({ + tableName: 'delegation_delegation_type', + timestamps: false, + indexes: [ + { + fields: ['delegation_id', 'delegation_type_id'], + unique: true, + }, + ], +}) +export class DelegationDelegationType extends Model< + InferAttributes, + InferCreationAttributes +> { + @ForeignKey(() => Delegation) + @Column({ + type: DataType.STRING, + primaryKey: true, + allowNull: false, + }) + delegationId!: string + + @ForeignKey(() => DelegationTypeModel) + @Column({ + type: DataType.STRING, + primaryKey: true, + allowNull: false, + }) + delegationTypeId!: string + + @Column({ + type: DataType.DATE, + allowNull: true, + }) + validTo?: Date + + @CreatedAt + readonly created!: CreationOptional + + @UpdatedAt + readonly modified?: Date + + @BelongsTo(() => Delegation) + delegation?: Delegation + + @BelongsTo(() => DelegationTypeModel) + delegationType?: DelegationTypeModel +} 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 51527569ba3b..36a578bce566 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 @@ -24,6 +24,7 @@ import { ClientDelegationType } from '../../clients/models/client-delegation-typ 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' +import { DelegationDelegationType } from './delegation-delegation-type.model' @Table({ tableName: 'delegation_type', @@ -79,6 +80,9 @@ export class DelegationTypeModel extends Model< @HasMany(() => PersonalRepresentativeDelegationTypeModel) prDelegationType?: PersonalRepresentativeDelegationTypeModel[] + @HasMany(() => DelegationDelegationType) + delegationDelegationTypes?: DelegationDelegationType[] + toDTO(): DelegationTypeDto { return { id: this.id, diff --git a/libs/auth-api-lib/src/lib/delegations/models/delegation.model.ts b/libs/auth-api-lib/src/lib/delegations/models/delegation.model.ts index fcb192dc7010..41ef58402d08 100644 --- a/libs/auth-api-lib/src/lib/delegations/models/delegation.model.ts +++ b/libs/auth-api-lib/src/lib/delegations/models/delegation.model.ts @@ -24,6 +24,7 @@ import { AuthDelegationProvider, AuthDelegationType, } from '@island.is/shared/types' +import { DelegationDelegationType } from './delegation-delegation-type.model' @Table({ tableName: 'delegation', @@ -45,7 +46,7 @@ export class Delegation extends Model< primaryKey: true, allowNull: false, }) - id!: CreationOptional + id!: string @Column({ type: DataType.STRING, @@ -119,7 +120,10 @@ export class Delegation extends Model< @HasMany(() => DelegationScope, { onDelete: 'cascade' }) delegationScopes?: NonAttribute - toDTO(): DelegationDTO { + @HasMany(() => DelegationDelegationType, { onDelete: 'cascade' }) + delegationDelegationTypes?: DelegationDelegationType[] + + toDTO(type = AuthDelegationType.Custom): DelegationDTO { return { id: this.id, fromName: this.fromDisplayName, @@ -131,19 +135,19 @@ export class Delegation extends Model< ? this.delegationScopes.map((scope) => scope.toDTO()) : [], provider: AuthDelegationProvider.Custom, - type: AuthDelegationType.Custom, + type: type, domainName: this.domainName, } } - toMergedDTO(): MergedDelegationDTO { + toMergedDTO(types = [AuthDelegationType.Custom]): MergedDelegationDTO { return { fromName: this.fromDisplayName, fromNationalId: this.fromNationalId, toNationalId: this.toNationalId, toName: this.toName, validTo: this.validTo, - types: [AuthDelegationType.Custom], + types: types, scopes: this.delegationScopes ? this.delegationScopes.map((scope) => scope.toDTO()) : [], diff --git a/libs/portals/admin/delegation-admin/project.json b/libs/portals/admin/delegation-admin/project.json index 5088d4044799..dbd46bfe137c 100644 --- a/libs/portals/admin/delegation-admin/project.json +++ b/libs/portals/admin/delegation-admin/project.json @@ -15,6 +15,12 @@ "jestConfig": "libs/portals/admin/delegation-admin/jest.config.ts" } }, + "extract-strings": { + "executor": "nx:run-commands", + "options": { + "command": "yarn ts-node -P libs/localization/tsconfig.lib.json libs/localization/scripts/extract 'libs/portals/admin/delegation-admin/src/**/*.{ts,tsx}'" + } + }, "codegen/frontend-client": { "executor": "nx:run-commands", "options": { diff --git a/libs/portals/shared-modules/delegations/src/components/access/AccessCard.tsx b/libs/portals/shared-modules/delegations/src/components/access/AccessCard.tsx index a7589e542044..ab3273fb365d 100644 --- a/libs/portals/shared-modules/delegations/src/components/access/AccessCard.tsx +++ b/libs/portals/shared-modules/delegations/src/components/access/AccessCard.tsx @@ -100,6 +100,9 @@ export const AccessCard = ({ let icon: IconType = 'people' switch (type) { + case AuthDelegationType.GeneralMandate: + label = formatMessage(m.delegationTypeGeneralMandate) + break case AuthDelegationType.LegalGuardian: label = formatMessage(m.delegationTypeLegalGuardian) break diff --git a/libs/portals/shared-modules/delegations/src/lib/messages.ts b/libs/portals/shared-modules/delegations/src/lib/messages.ts index 2ccb9eebed9b..ae37baa97455 100644 --- a/libs/portals/shared-modules/delegations/src/lib/messages.ts +++ b/libs/portals/shared-modules/delegations/src/lib/messages.ts @@ -30,6 +30,10 @@ export const m = defineMessages({ id: 'sp.access-control-delegations:delegation-type-legal-guardian', defaultMessage: 'Forsjá', }, + delegationTypeGeneralMandate: { + id: 'sp.access-control-delegations:delegation-type-general-mandate', + defaultMessage: 'Allsherjarumboð', + }, delegationTypeProcurationHolder: { id: 'sp.access-control-delegations:delegation-type-procuration-holder', defaultMessage: 'Prókúra', diff --git a/libs/services/auth/testing/src/fixtures/delegation.fixture.ts b/libs/services/auth/testing/src/fixtures/delegation.fixture.ts index 84d055729221..fe86b56faebd 100644 --- a/libs/services/auth/testing/src/fixtures/delegation.fixture.ts +++ b/libs/services/auth/testing/src/fixtures/delegation.fixture.ts @@ -16,7 +16,7 @@ export interface CreateDelegationOptions { today?: Date expired?: boolean future?: boolean - domainName?: string + domainName?: string | null } export type CreateDelegationScope = Pick< From 4e76ba99e529f3bb989a27df4d1ce2077a9850bf Mon Sep 17 00:00:00 2001 From: birkirkristmunds <142495885+birkirkristmunds@users.noreply.github.com> Date: Mon, 23 Sep 2024 22:44:48 +0000 Subject: [PATCH 18/18] fix(skilavottord): Fix issue with numberplate count (#16120) * TS-916 Fix issue with numberplate count * TS-916 Fix code after code rabbit review * TS-916 Fix code after code rabbit review --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../CarDetailsBox2/CarDetailsBox2.tsx | 1 + .../DeregisterVehicle/Confirm/Confirm.tsx | 24 ++++++++++++++++--- .../app/modules/vehicle/vehicle.resolver.ts | 2 +- .../app/modules/vehicle/vehicle.service.ts | 3 +-- 4 files changed, 24 insertions(+), 6 deletions(-) diff --git a/apps/skilavottord/web/components/CarDetailsBox2/CarDetailsBox2.tsx b/apps/skilavottord/web/components/CarDetailsBox2/CarDetailsBox2.tsx index b76419da7cf3..546459bafcb3 100644 --- a/apps/skilavottord/web/components/CarDetailsBox2/CarDetailsBox2.tsx +++ b/apps/skilavottord/web/components/CarDetailsBox2/CarDetailsBox2.tsx @@ -33,6 +33,7 @@ interface BoxProps { vinNumber?: string outInStatus: number useStatus: string + reloadFlag: boolean // To force reload of the component to make sure the data in the parent is correct } export const CarDetailsBox2: FC> = ({ diff --git a/apps/skilavottord/web/screens/DeregisterVehicle/Confirm/Confirm.tsx b/apps/skilavottord/web/screens/DeregisterVehicle/Confirm/Confirm.tsx index 293ac53554ed..53333b718288 100644 --- a/apps/skilavottord/web/screens/DeregisterVehicle/Confirm/Confirm.tsx +++ b/apps/skilavottord/web/screens/DeregisterVehicle/Confirm/Confirm.tsx @@ -1,7 +1,7 @@ import { useMutation, useQuery } from '@apollo/client' import gql from 'graphql-tag' import { useRouter } from 'next/router' -import React, { FC, useContext, useEffect } from 'react' +import React, { FC, useContext, useEffect, useState } from 'react' import { Box, @@ -33,7 +33,7 @@ import { Role, } from '@island.is/skilavottord-web/graphql/schema' import { useI18n } from '@island.is/skilavottord-web/i18n' -import { OutInUsage } from '@island.is/skilavottord-web/utils/consts' +import { OutInUsage, UseStatus } from '@island.is/skilavottord-web/utils/consts' import { getYear } from '@island.is/skilavottord-web/utils/dateUtils' import { FormProvider, useForm } from 'react-hook-form' @@ -100,6 +100,17 @@ const UpdateSkilavottordVehicleInfoMutation = gql` ` const Confirm: FC> = () => { + const [reloadFlag, setReloadFlag] = useState(false) + + // Update reloadFlag to trigger the child component to reload + const triggerReload = () => { + setReloadFlag(true) + } + + useEffect(() => { + triggerReload() + }, [setReloadFlag]) + const methods = useForm({ mode: 'onChange', }) @@ -184,6 +195,7 @@ const Confirm: FC> = () => { const handleConfirm = () => { let newMileage = mileageValue + let plateCount = plateCountValue if (mileageValue !== undefined) { newMileage = +mileageValue.trim().replace(/\./g, '') @@ -191,12 +203,17 @@ const Confirm: FC> = () => { newMileage = vehicle?.mileage } + // If vehicle is out of use and not using ticket, set plate count to 0 + if (outInStatus === OutInUsage.OUT && useStatus !== UseStatus.OUT_TICKET) { + plateCount = 0 + } + // Update vehicle table with latests information setVehicleRequest({ variables: { permno: vehicle?.vehicleId, mileage: newMileage, - plateCount: plateCountValue === 0 ? 0 : plateCountValue, + plateCount, plateLost: !!plateLost?.length, }, }).then(() => { @@ -274,6 +291,7 @@ const Confirm: FC> = () => { mileage={vehicle.mileage || 0} outInStatus={outInStatus} useStatus={useStatus || ''} + reloadFlag={reloadFlag} /> diff --git a/apps/skilavottord/ws/src/app/modules/vehicle/vehicle.resolver.ts b/apps/skilavottord/ws/src/app/modules/vehicle/vehicle.resolver.ts index a5f2af14ec32..d8a1516ffda1 100644 --- a/apps/skilavottord/ws/src/app/modules/vehicle/vehicle.resolver.ts +++ b/apps/skilavottord/ws/src/app/modules/vehicle/vehicle.resolver.ts @@ -108,7 +108,7 @@ export class VehicleResolver { @CurrentUser() user: User, @Args('permno') permno: string, @Args('mileage') mileage: number, - @Args('plateCount') plateCount: number, + @Args('plateCount', { nullable: true }) plateCount: number, @Args('plateLost') plateLost: boolean, ) { return await this.vehicleService.updateVehicleInfo( diff --git a/apps/skilavottord/ws/src/app/modules/vehicle/vehicle.service.ts b/apps/skilavottord/ws/src/app/modules/vehicle/vehicle.service.ts index 3be43b3a65b8..67729e02c3a9 100644 --- a/apps/skilavottord/ws/src/app/modules/vehicle/vehicle.service.ts +++ b/apps/skilavottord/ws/src/app/modules/vehicle/vehicle.service.ts @@ -69,7 +69,7 @@ export class VehicleService { const findVehicle = await this.findByVehicleId(permno) if (findVehicle) { findVehicle.mileage = mileage ?? 0 - findVehicle.plateCount = plateCount ?? 0 + findVehicle.plateCount = plateCount findVehicle.plateLost = plateLost await findVehicle.save() @@ -85,7 +85,6 @@ export class VehicleService { throw new Error(errorMsg) } } - async create(vehicle: VehicleModel): Promise { try { // check if vehicle is already in db