diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a740b9f7fb3c..341480407d13 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -159,6 +159,7 @@ codemagic.yaml /libs/api/domains/official-journal-of-iceland-application/ @island-is/hugsmidjan /libs/api/domains/document-provider/ @island-is/hugsmidjan @island-is/core /libs/api/domains/housing-benefits/ @island-is/hugsmidjan +/libs/api/domains/law-and-order/ @island-is/hugsmidjan /libs/clients/documents/ @island-is/hugsmidjan /libs/clients/documents-v2/ @island-is/hugsmidjan /libs/clients/finance/ @island-is/hugsmidjan @@ -198,6 +199,7 @@ codemagic.yaml /libs/portals/admin/air-discount-scheme @island-is/hugsmidjan /libs/application/templates/official-journal-of-iceland/ @island-is/hugsmidjan /libs/application/template-api-modules/src/lib/modules/templates/official-journal-of-iceland/ @island-is/hugsmidjan +/libs/clients/judicial-system-sp/ @island-is/hugsmidjan /libs/application/templates/data-protection-complaint/ @island-is/norda /libs/application/templates/institution-collaboration/ @island-is/norda @island-is/fuglar /libs/application/templates/login-service/ @island-is/norda diff --git a/apps/api/infra/api.ts b/apps/api/infra/api.ts index ca9a1b261d2d..5e14a7d5aed7 100644 --- a/apps/api/infra/api.ts +++ b/apps/api/infra/api.ts @@ -46,6 +46,7 @@ import { UniversityCareers, OfficialJournalOfIceland, OfficialJournalOfIcelandApplication, + JudicialSystemServicePortal, Frigg, HealthDirectorateOrganDonation, HealthDirectorateVaccination, @@ -432,6 +433,7 @@ export const serviceSetup = (services: { SignatureCollection, SocialInsuranceAdministration, OfficialJournalOfIceland, + JudicialSystemServicePortal, OfficialJournalOfIcelandApplication, Frigg, HealthDirectorateOrganDonation, diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts index 885e71d4dc1f..97ed2726f24f 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -188,7 +188,9 @@ import { } from '@island.is/clients/university-careers' import { HousingBenefitsConfig } from '@island.is/clients/hms-housing-benefits' import { UserProfileClientConfig } from '@island.is/clients/user-profile' +import { LawAndOrderModule } from '@island.is/api/domains/law-and-order' import { UltravioletRadiationClientConfig } from '@island.is/clients/ultraviolet-radiation' +import { JudicialSystemSPClientConfig } from '@island.is/clients/judicial-system-sp' import { CriminalRecordClientConfig } from '@island.is/clients/criminal-record' import { HealthInsuranceV2ClientConfig } from '@island.is/clients/icelandic-health-insurance/health-insurance' import { VmstClientConfig } from '@island.is/clients/vmst' @@ -328,6 +330,7 @@ const environment = getConfig AuthAdminModule, HousingBenefitCalculatorModule, SignatureCollectionModule, + LawAndOrderModule, UmbodsmadurSkuldaraModule, HealthDirectorateModule, ConfigModule.forRoot({ @@ -419,11 +422,12 @@ const environment = getConfig LicenseConfig, UserProfileClientConfig, UltravioletRadiationClientConfig, + JudicialSystemSPClientConfig, FriggClientConfig, GradeClientConfig, VmstClientConfig, - HealthInsuranceV2ClientConfig, CriminalRecordClientConfig, + HealthInsuranceV2ClientConfig, UmbodsmadurSkuldaraClientConfig, emailModuleConfig, ], diff --git a/apps/service-portal/src/auth.ts b/apps/service-portal/src/auth.ts index 9ccc5a92278d..d3ea87433d36 100644 --- a/apps/service-portal/src/auth.ts +++ b/apps/service-portal/src/auth.ts @@ -38,6 +38,7 @@ const SERVICE_PORTAL_SCOPES = [ ApiScope.internal, ApiScope.internalProcuring, ApiScope.meDetails, + ApiScope.lawAndOrder, ApiScope.licenses, ApiScope.licensesVerify, ApiScope.company, diff --git a/apps/service-portal/src/components/ContentBreadcrumbs/ContentBreadcrumbs.tsx b/apps/service-portal/src/components/ContentBreadcrumbs/ContentBreadcrumbs.tsx index 7c7771d92102..58f9ea47b936 100644 --- a/apps/service-portal/src/components/ContentBreadcrumbs/ContentBreadcrumbs.tsx +++ b/apps/service-portal/src/components/ContentBreadcrumbs/ContentBreadcrumbs.tsx @@ -105,10 +105,8 @@ const ContentBreadcrumbs: FC> = () => { } findBreadcrumbsPath(navigation, []) - const isMobile = width < theme.breakpoints.md if (items.length < 2) return null - return ( > = () => { const { userInfo } = useAuth() @@ -51,6 +53,24 @@ export const Dashboard: FC> = () => { const IS_COMPANY = userInfo?.profile?.subjectType === 'legalEntity' const hasDelegationAccess = userInfo?.scopes?.includes(DocumentsScope.main) + // Versioning feature flag. Remove after feature is live. + const [v3Enabled, setV3Enabled] = useState() + + const featureFlagClient = useFeatureFlagClient() + useEffect(() => { + const isFlagEnabled = async () => { + const ffEnabled = await featureFlagClient.getValue( + `isServicePortalDocumentsV3PageEnabled`, + false, + ) + if (ffEnabled) { + setV3Enabled(ffEnabled as boolean) + } + } + isFlagEnabled() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + useEffect(() => { PlausiblePageviewDetail( ServicePortalPaths.Root, @@ -219,22 +239,41 @@ export const Dashboard: FC> = () => { ) : filteredDocuments.length > 0 ? ( filteredDocuments.map((doc, i) => ( - + {v3Enabled ? ( + + ) : ( + + )} )) ) : ( diff --git a/charts/islandis/values.dev.yaml b/charts/islandis/values.dev.yaml index 54fe3e284c71..bf816cdfa428 100644 --- a/charts/islandis/values.dev.yaml +++ b/charts/islandis/values.dev.yaml @@ -368,6 +368,7 @@ api: XROAD_ICELAND_UNIVERSITY_OF_THE_ARTS_PATH: 'IS-DEV/EDU/10049/LHI-Protected/brautskraning-v1' XROAD_INNA_PATH: 'IS-DEV/GOV/10066/MMS-Protected/inna-v1' XROAD_INTELLECTUAL_PROPERTIES_PATH: 'IS-DEV/GOV/10030/WebAPI-Public/HUG-webAPI/' + XROAD_JUDICIAL_SYSTEM_SP_PATH: 'IS-DEV/GOV/10014/Rettarvorslugatt-Private/judicial-system-mailbox-api' XROAD_MMS_FRIGG_PATH: 'IS-DEV/GOV/10066/MMS-Protected/frigg-api' XROAD_MMS_GRADE_SERVICE_ID: 'IS-DEV/GOV/10066/MMS-Protected/grade-api-v1' XROAD_MMS_LICENSE_SERVICE_ID: 'IS-DEV/GOV/10066/MMS-Protected/license-api-v1' diff --git a/charts/islandis/values.prod.yaml b/charts/islandis/values.prod.yaml index 92b9250b3f09..1c7e76c77b9c 100644 --- a/charts/islandis/values.prod.yaml +++ b/charts/islandis/values.prod.yaml @@ -358,6 +358,7 @@ api: XROAD_ICELAND_UNIVERSITY_OF_THE_ARTS_PATH: 'IS/EDU/4210984099/LHI-Protected/brautskraning-v1' XROAD_INNA_PATH: 'IS/GOV/6601241280/MMS-Protected/inna-v1' XROAD_INTELLECTUAL_PROPERTIES_PATH: 'IS/GOV/6501912189/WebAPI-Public/HUG-webAPI/' + XROAD_JUDICIAL_SYSTEM_SP_PATH: 'IS-GOV/GOV/5804170510/Rettarvorslugatt-Private/judicial-system-mailbox-api' XROAD_MMS_FRIGG_PATH: 'IS/GOV/10066/MMS-Protected/frigg-api' XROAD_MMS_GRADE_SERVICE_ID: 'IS/GOV/6601241280/MMS-Protected/grade-api-v1' XROAD_MMS_LICENSE_SERVICE_ID: 'IS/GOV/6601241280/MMS-Protected/license-api-v1' diff --git a/charts/islandis/values.staging.yaml b/charts/islandis/values.staging.yaml index 5fbb15a89a49..60eca44050fa 100644 --- a/charts/islandis/values.staging.yaml +++ b/charts/islandis/values.staging.yaml @@ -368,6 +368,7 @@ api: XROAD_ICELAND_UNIVERSITY_OF_THE_ARTS_PATH: 'IS-TEST/EDU/10049/LHI-Protected/brautskraning-v1' XROAD_INNA_PATH: 'IS-TEST/GOV/6601241280/MMS-Protected/inna-v1' XROAD_INTELLECTUAL_PROPERTIES_PATH: 'IS-TEST/GOV/6501912189/WebAPI-Public/HUG-webAPI/' + XROAD_JUDICIAL_SYSTEM_SP_PATH: 'IS-TEST/GOV/10014/Rettarvorslugatt-Private/judicial-system-mailbox-api' XROAD_MMS_FRIGG_PATH: 'IS-TEST/GOV/10066/MMS-Protected/frigg-api' XROAD_MMS_GRADE_SERVICE_ID: 'IS-TEST/GOV/6601241280/MMS-Protected/grade-api-v1' XROAD_MMS_LICENSE_SERVICE_ID: 'IS-TEST/GOV/6601241280/MMS-Protected/license-api-v1' diff --git a/infra/src/dsl/xroad.ts b/infra/src/dsl/xroad.ts index 0dc7df744772..1f9a79557692 100644 --- a/infra/src/dsl/xroad.ts +++ b/infra/src/dsl/xroad.ts @@ -841,6 +841,17 @@ export const UniversityGatewayReykjavikUniversity = new XroadConf({ }, }) +export const JudicialSystemServicePortal = new XroadConf({ + env: { + XROAD_JUDICIAL_SYSTEM_SP_PATH: { + dev: 'IS-DEV/GOV/10014/Rettarvorslugatt-Private/judicial-system-mailbox-api', + staging: + 'IS-TEST/GOV/10014/Rettarvorslugatt-Private/judicial-system-mailbox-api', + prod: 'IS-GOV/GOV/5804170510/Rettarvorslugatt-Private/judicial-system-mailbox-api', + }, + }, +}) + export const SocialInsuranceAdministration = new XroadConf({ env: { XROAD_TR_PATH: { diff --git a/libs/api/domains/documents/src/lib/documentV2.resolver.ts b/libs/api/domains/documents/src/lib/documentV2.resolver.ts index 548c858f4ac5..406ae800989a 100644 --- a/libs/api/domains/documents/src/lib/documentV2.resolver.ts +++ b/libs/api/domains/documents/src/lib/documentV2.resolver.ts @@ -29,6 +29,7 @@ import { MailActionInput } from './models/v2/bulkMailAction.input' import { DocumentMailAction } from './models/v2/mailAction.model.' import { LOGGER_PROVIDER, type Logger } from '@island.is/logging' import { DocumentV2MarkAllMailAsRead } from './models/v2/markAllMailAsRead.model' +import type { Locale } from '@island.is/shared/types' const LOG_CATEGORY = 'documents-resolver' @@ -46,6 +47,8 @@ export class DocumentResolverV2 { @Query(() => DocumentV2, { nullable: true, name: 'documentV2' }) async documentV2( @Args('input') input: DocumentInput, + @Args('locale', { type: () => String, nullable: true }) + locale: Locale = 'is', @CurrentUser() user: User, ): Promise { try { @@ -55,8 +58,14 @@ export class DocumentResolverV2 { namespace: '@island.is/api/document-v2', action: 'getDocument', resources: input.id, + meta: { includeDocument: input.includeDocument }, }, - this.documentServiceV2.findDocumentById(user.nationalId, input.id), + this.documentServiceV2.findDocumentById( + user.nationalId, + input.id, + locale, + input.includeDocument, + ), ) } catch (e) { this.logger.info('failed to get single document', { diff --git a/libs/api/domains/documents/src/lib/documentV2.service.ts b/libs/api/domains/documents/src/lib/documentV2.service.ts index c408c07a9acd..351ddc4055f2 100644 --- a/libs/api/domains/documents/src/lib/documentV2.service.ts +++ b/libs/api/domains/documents/src/lib/documentV2.service.ts @@ -1,5 +1,8 @@ import { Inject, Injectable } from '@nestjs/common' -import { DocumentsClientV2Service } from '@island.is/clients/documents-v2' +import { + DocumentsClientV2Service, + MessageAction, +} from '@island.is/clients/documents-v2' import { LOGGER_PROVIDER, type Logger } from '@island.is/logging' import { isDefined } from '@island.is/shared/utils' import { Category } from './models/v2/category.model' @@ -8,6 +11,7 @@ import { PaginatedDocuments, Document, DocumentPageNumber, + Action, } from './models/v2/document.model' import type { ConfigType } from '@island.is/nest/config' import { DocumentsInput } from './models/v2/documents.input' @@ -32,10 +36,14 @@ export class DocumentServiceV2 { async findDocumentById( nationalId: string, documentId: string, + locale?: string, + includeDocument?: boolean, ): Promise { const document = await this.documentService.getCustomersDocument( nationalId, documentId, + locale, + includeDocument, ) if (!document) { @@ -56,7 +64,13 @@ export class DocumentServiceV2 { default: type = FileType.UNKNOWN } - + const confirmation = document.actions?.find( + (action) => action.type === 'confirmation', + ) + const alert = document.actions?.find((action) => action.type === 'alert') + const actions = document.actions?.filter( + (action) => action.type !== 'alert' && action.type !== 'confirmation', + ) return { ...document, publicationDate: document.date, @@ -67,10 +81,16 @@ export class DocumentServiceV2 { id: document.senderNationalId, name: document.senderName, }, - content: { - type, - value: document.content, - }, + content: document.content + ? { + type, + value: document.content, + } + : undefined, + isUrgent: document.urgent, + actions: this.actionMapper(documentId, actions), + confirmation: confirmation, + alert: alert, } } @@ -107,7 +127,6 @@ export class DocumentServiceV2 { totalCount: documents?.totalCount, }) } - const documentData: Array = documents?.documents .map((d) => { @@ -123,6 +142,7 @@ export class DocumentServiceV2 { name: d.senderName, id: d.senderNationalId, }, + isUrgent: d.urgent, } }) .filter(isDefined) ?? [] @@ -331,4 +351,46 @@ export class DocumentServiceV2 { throw new Error('Invalid single document action') } } + + private actionMapper = (id: string, actions?: Array) => { + if (actions === undefined) return undefined + const hasEmpty = actions.every( + (x) => + x?.data === undefined || + x.title === undefined || + x.title === '' || + x.data === '', + ) + + // we return the document even if the actions are faulty, logged for tracability + if (hasEmpty) { + this.logger.warn('No title or data in actions array', { + category: LOG_CATEGORY, + id, + }) + return undefined + } + + const mapped: Array = actions?.map((x) => { + if (x.type === 'file') { + return { + ...x, + icon: 'download', + data: `${this.downloadServiceConfig.baseUrl}/download/v1/electronic-documents/${x.data}`, // if type file, we download + } + } + if (x.type === 'url') { + return { + ...x, + icon: 'open', + } + } + return { + ...x, + icon: 'receipt', + } + }) + + return mapped + } } diff --git a/libs/api/domains/documents/src/lib/models/v2/document.input.ts b/libs/api/domains/documents/src/lib/models/v2/document.input.ts index c9ecda252413..eca763e42f77 100644 --- a/libs/api/domains/documents/src/lib/models/v2/document.input.ts +++ b/libs/api/domains/documents/src/lib/models/v2/document.input.ts @@ -14,4 +14,7 @@ export class DocumentInput { @Field({ defaultValue: 10 }) readonly pageSize!: number + + @Field(() => Boolean, { nullable: true, defaultValue: true }) + readonly includeDocument?: boolean } diff --git a/libs/api/domains/documents/src/lib/models/v2/document.model.ts b/libs/api/domains/documents/src/lib/models/v2/document.model.ts index 2516cca5d31e..adf37db1ff02 100644 --- a/libs/api/domains/documents/src/lib/models/v2/document.model.ts +++ b/libs/api/domains/documents/src/lib/models/v2/document.model.ts @@ -5,6 +5,21 @@ import { PaginatedResponse } from '@island.is/nest/pagination' import { Category } from './category.model' import { Type } from './type.model' +@ObjectType('DocumentV2Action') +export class Action { + @Field({ nullable: true }) + title?: string + + @Field({ nullable: true }) + type?: string + + @Field({ nullable: true }) + data?: string + + @Field({ nullable: true }) + icon?: string +} + @ObjectType('DocumentV2') export class Document { @Field(() => ID) @@ -45,6 +60,18 @@ export class Document { description: 'URL in download service. For downloading PDFs', }) downloadUrl?: string + + @Field(() => Action, { nullable: true }) + alert?: Action + + @Field(() => Action, { nullable: true }) + confirmation?: Action + + @Field(() => [Action], { nullable: true }) + actions?: Array + + @Field(() => Boolean, { nullable: true }) + isUrgent?: boolean } @ObjectType('DocumentsV2') diff --git a/libs/api/domains/law-and-order/.eslintrc.json b/libs/api/domains/law-and-order/.eslintrc.json new file mode 100644 index 000000000000..632e9b0e2225 --- /dev/null +++ b/libs/api/domains/law-and-order/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["../../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/api/domains/law-and-order/README.md b/libs/api/domains/law-and-order/README.md new file mode 100644 index 000000000000..ccb45690345e --- /dev/null +++ b/libs/api/domains/law-and-order/README.md @@ -0,0 +1,7 @@ +# api-domains-law-and-order + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test api-domains-law-and-order` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/api/domains/law-and-order/jest.config.ts b/libs/api/domains/law-and-order/jest.config.ts new file mode 100644 index 000000000000..41231ed358ef --- /dev/null +++ b/libs/api/domains/law-and-order/jest.config.ts @@ -0,0 +1,11 @@ +/* eslint-disable */ +export default { + displayName: 'api-domains-law-and-order', + preset: '../../../../jest.preset.js', + testEnvironment: 'node', + transform: { + '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../../../coverage/libs/api/domains/law-and-order', +} diff --git a/libs/api/domains/law-and-order/project.json b/libs/api/domains/law-and-order/project.json new file mode 100644 index 000000000000..51777b320560 --- /dev/null +++ b/libs/api/domains/law-and-order/project.json @@ -0,0 +1,30 @@ +{ + "name": "api-domains-law-and-order", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/api/domains/law-and-order/src", + "projectType": "library", + "targets": { + "lint": { + "executor": "@nx/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["libs/api/domains/law-and-order/**/*.ts"] + } + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/api/domains/law-and-order/jest.config.ts", + "passWithNoTests": true + }, + "configurations": { + "ci": { + "ci": true, + "codeCoverage": true + } + } + } + }, + "tags": ["lib:api", "scope:api"] +} diff --git a/libs/api/domains/law-and-order/src/dto/getCourtCaseInput.ts b/libs/api/domains/law-and-order/src/dto/getCourtCaseInput.ts new file mode 100644 index 000000000000..fd0486bfb117 --- /dev/null +++ b/libs/api/domains/law-and-order/src/dto/getCourtCaseInput.ts @@ -0,0 +1,9 @@ +import { Field, InputType } from '@nestjs/graphql' +import { IsString } from 'class-validator' + +@InputType('LawAndOrderCourtCaseInput') +export class GetCourtCaseInput { + @Field() + @IsString() + id!: string +} diff --git a/libs/api/domains/law-and-order/src/dto/getSubpoenaInput.ts b/libs/api/domains/law-and-order/src/dto/getSubpoenaInput.ts new file mode 100644 index 000000000000..3937f25c4c92 --- /dev/null +++ b/libs/api/domains/law-and-order/src/dto/getSubpoenaInput.ts @@ -0,0 +1,9 @@ +import { Field, InputType } from '@nestjs/graphql' +import { IsString } from 'class-validator' + +@InputType('LawAndOrderSubpoenaInput') +export class GetSubpoenaInput { + @Field() + @IsString() + id!: string +} diff --git a/libs/api/domains/law-and-order/src/dto/postDefenseChoiceInput.model.ts b/libs/api/domains/law-and-order/src/dto/postDefenseChoiceInput.model.ts new file mode 100644 index 000000000000..b29cf5bb0223 --- /dev/null +++ b/libs/api/domains/law-and-order/src/dto/postDefenseChoiceInput.model.ts @@ -0,0 +1,17 @@ +import { Field, ID, InputType } from '@nestjs/graphql' +import { IsOptional, IsString } from 'class-validator' +import { DefenseChoiceEnum } from '../models/defenseChoiceEnum.model' + +@InputType('LawAndOrderDefenseChoiceInput') +export class PostDefenseChoiceInput { + @Field(() => ID) + @IsString() + caseId!: string + + @Field(() => DefenseChoiceEnum) + choice!: DefenseChoiceEnum + + @Field({ nullable: true }) + @IsOptional() + lawyersNationalId?: string +} diff --git a/libs/api/domains/law-and-order/src/index.ts b/libs/api/domains/law-and-order/src/index.ts new file mode 100644 index 000000000000..03ccf92e2560 --- /dev/null +++ b/libs/api/domains/law-and-order/src/index.ts @@ -0,0 +1 @@ +export { LawAndOrderModule } from './lib/law-and-order.module' diff --git a/libs/api/domains/law-and-order/src/lib/helpers/mappers.ts b/libs/api/domains/law-and-order/src/lib/helpers/mappers.ts new file mode 100644 index 000000000000..fc736be669e9 --- /dev/null +++ b/libs/api/domains/law-and-order/src/lib/helpers/mappers.ts @@ -0,0 +1,118 @@ +import { + DefenderInfoDefenderChoiceEnum, + StateTagColorEnum, + UpdateSubpoenaDtoDefenderChoiceEnum, +} from '@island.is/clients/judicial-system-sp' +import { CourtCaseStateTagColorEnum } from '../../models/courtCases.model' +import { DefenseChoiceEnum } from '../../models/defenseChoiceEnum.model' + +// Maps the application's internal representation of defense choices to the judicial system's representation. +export const mapDefenseChoice = ( + choice: DefenseChoiceEnum, +): UpdateSubpoenaDtoDefenderChoiceEnum => { + switch (choice) { + // Each case maps a local enum value to the corresponding value in the judicial system's enum. + case DefenseChoiceEnum.CHOOSE: + return UpdateSubpoenaDtoDefenderChoiceEnum.CHOOSE + case DefenseChoiceEnum.WAIVE: + return UpdateSubpoenaDtoDefenderChoiceEnum.WAIVE + case DefenseChoiceEnum.DELAY: + return UpdateSubpoenaDtoDefenderChoiceEnum.DELAY + case DefenseChoiceEnum.DELEGATE: + return UpdateSubpoenaDtoDefenderChoiceEnum.DELEGATE + default: + // Provides a default mapping if the input doesn't match any known value. + return UpdateSubpoenaDtoDefenderChoiceEnum.DELAY + } +} + +// Maps the application's internal representation of defense choices to the judicial system's representation. +export const mapDefenseChoiceForSubpoena = ( + choice?: DefenderInfoDefenderChoiceEnum, +): DefenseChoiceEnum => { + switch (choice) { + // Each case maps a local enum value to the corresponding value in the judicial system's enum. + case DefenderInfoDefenderChoiceEnum.CHOOSE: + return DefenseChoiceEnum.CHOOSE + case DefenderInfoDefenderChoiceEnum.WAIVE: + return DefenseChoiceEnum.WAIVE + case DefenderInfoDefenderChoiceEnum.DELAY: + return DefenseChoiceEnum.DELAY + case DefenderInfoDefenderChoiceEnum.DELEGATE: + return DefenseChoiceEnum.DELEGATE + default: + // Provides a default mapping if the input doesn't match any known value. + return DefenseChoiceEnum.DELAY + } +} + +export const mapTagTypes = ( + color?: StateTagColorEnum, +): CourtCaseStateTagColorEnum => { + switch (color) { + case StateTagColorEnum.Blue: + return CourtCaseStateTagColorEnum.blue + case StateTagColorEnum.Blueberry: + return CourtCaseStateTagColorEnum.blueberry + case StateTagColorEnum.DarkerBlue: + return CourtCaseStateTagColorEnum.darkerBlue + case StateTagColorEnum.Disabled: + return CourtCaseStateTagColorEnum.disabled + case StateTagColorEnum.Dark: + return CourtCaseStateTagColorEnum.dark + case StateTagColorEnum.Mint: + return CourtCaseStateTagColorEnum.mint + case StateTagColorEnum.Purple: + return CourtCaseStateTagColorEnum.purple + case StateTagColorEnum.Red: + return CourtCaseStateTagColorEnum.red + case StateTagColorEnum.Rose: + return CourtCaseStateTagColorEnum.rose + case StateTagColorEnum.Warn: + return CourtCaseStateTagColorEnum.warn + case StateTagColorEnum.White: + return CourtCaseStateTagColorEnum.white + case StateTagColorEnum.Yellow: + return CourtCaseStateTagColorEnum.yellow + + default: + return CourtCaseStateTagColorEnum.blue + } +} + +interface Choice { + message: { + id: string + defaultMessage: string + } +} + +// Get localized messages for defense choices in Subpoena +export const DefenseChoices: Record = { + WAIVE: { + message: { + id: 'api.law-and-order:no-defender', + defaultMessage: 'Ég óska ekki eftir verjanda', + }, + }, + CHOOSE: { + message: { + id: 'api.law-and-order:choosing-lawyer', + defaultMessage: + 'Ég óska þess að valinn lögmaður verði skipaður verjandi minn', + }, + }, + DELAY: { + message: { + id: 'api.law-and-order:delay-choice', + defaultMessage: + 'Ég óska eftir fresti fram að þingfestingu til þess að tilnefna verjanda', + }, + }, + DELEGATE: { + message: { + id: 'api.law-and-order:choose-for-me', + defaultMessage: 'Ég fel dómara málsins að tilnefna og skipa mér verjanda', + }, + }, +} diff --git a/libs/api/domains/law-and-order/src/lib/law-and-order.module.ts b/libs/api/domains/law-and-order/src/lib/law-and-order.module.ts new file mode 100644 index 000000000000..56f839d22865 --- /dev/null +++ b/libs/api/domains/law-and-order/src/lib/law-and-order.module.ts @@ -0,0 +1,18 @@ +import { Module } from '@nestjs/common' +import { AuthModule } from '@island.is/auth-nest-tools' +import { JudicialSystemSPClientModule } from '@island.is/clients/judicial-system-sp' +import { CmsTranslationsModule } from '@island.is/cms-translations' +import { FeatureFlagModule } from '@island.is/nest/feature-flags' +import { LawAndOrderResolver } from './law-and-order.resolver' +import { LawAndOrderService } from './law-and-order.service' + +@Module({ + imports: [ + JudicialSystemSPClientModule, + AuthModule, + FeatureFlagModule, + CmsTranslationsModule, + ], + providers: [LawAndOrderResolver, LawAndOrderService], +}) +export class LawAndOrderModule {} diff --git a/libs/api/domains/law-and-order/src/lib/law-and-order.resolver.ts b/libs/api/domains/law-and-order/src/lib/law-and-order.resolver.ts new file mode 100644 index 000000000000..f02dee2dcd49 --- /dev/null +++ b/libs/api/domains/law-and-order/src/lib/law-and-order.resolver.ts @@ -0,0 +1,97 @@ +import type { User } 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 { Audit } from '@island.is/nest/audit' +import { + FeatureFlag, + FeatureFlagGuard, + Features, +} from '@island.is/nest/feature-flags' +import type { Locale } from '@island.is/shared/types' +import { UseGuards } from '@nestjs/common' +import { Args, Mutation, Query, Resolver } from '@nestjs/graphql' +import { GetCourtCaseInput } from '../dto/getCourtCaseInput' +import { GetSubpoenaInput } from '../dto/getSubpoenaInput' +import { PostDefenseChoiceInput } from '../dto/postDefenseChoiceInput.model' +import { CourtCase } from '../models/courtCase.model' +import { CourtCases } from '../models/courtCases.model' +import { DefenseChoice } from '../models/defenseChoice.model' +import { Lawyers } from '../models/lawyers.model' +import { Subpoena } from '../models/subpoena.model' +import { LawAndOrderService } from './law-and-order.service' + +@UseGuards(IdsUserGuard, ScopesGuard, FeatureFlagGuard) +@Resolver() +@Audit({ namespace: '@island.is/api/law-and-order' }) +@FeatureFlag(Features.servicePortalLawAndOrderModuleEnabled) +@Scopes(ApiScope.lawAndOrder) +export class LawAndOrderResolver { + constructor(private readonly lawAndOrderService: LawAndOrderService) {} + + @Query(() => CourtCases, { + name: 'lawAndOrderCourtCasesList', + nullable: true, + }) + @Audit() + getCourtCasesList( + @CurrentUser() user: User, + @Args('locale', { type: () => String, nullable: true }) + locale: Locale = 'is', + ) { + return this.lawAndOrderService.getCourtCases(user, locale) + } + + @Query(() => CourtCase, { + name: 'lawAndOrderCourtCaseDetail', + nullable: true, + }) + @Audit() + getCourtCaseDetail( + @CurrentUser() user: User, + @Args('input') input: GetCourtCaseInput, + @Args('locale', { type: () => String, nullable: true }) + locale: Locale = 'is', + ) { + return this.lawAndOrderService.getCourtCase(user, input.id, locale) + } + + @Query(() => Subpoena, { name: 'lawAndOrderSubpoena', nullable: true }) + @Audit() + getSubpoena( + @CurrentUser() user: User, + @Args('input') input: GetSubpoenaInput, + @Args('locale', { type: () => String, nullable: true }) + locale: Locale = 'is', + ) { + return this.lawAndOrderService.getSubpoena(user, input.id, locale) + } + + @Query(() => Lawyers, { name: 'lawAndOrderLawyers', nullable: true }) + @Audit() + getLawyers( + @CurrentUser() user: User, + @Args('locale', { type: () => String, nullable: true }) + locale: Locale = 'is', + ) { + return this.lawAndOrderService.getLawyers(user, locale) + } + + @Mutation(() => DefenseChoice, { + name: 'lawAndOrderDefenseChoicePost', + nullable: true, + }) + @Audit() + postDefenseChoice( + @Args('input') input: PostDefenseChoiceInput, + @Args('locale', { type: () => String, nullable: true }) + locale: Locale = 'is', + @CurrentUser() user: User, + ) { + return this.lawAndOrderService.postDefenseChoice(user, input, locale) + } +} diff --git a/libs/api/domains/law-and-order/src/lib/law-and-order.service.ts b/libs/api/domains/law-and-order/src/lib/law-and-order.service.ts new file mode 100644 index 000000000000..3b38f29db9b1 --- /dev/null +++ b/libs/api/domains/law-and-order/src/lib/law-and-order.service.ts @@ -0,0 +1,201 @@ +import type { User } from '@island.is/auth-nest-tools' +import { + AlertMessageTypeEnum, + CasesResponse, + Defender, + JudicialSystemSPClientService, + SubpoenaResponse, +} from '@island.is/clients/judicial-system-sp' +import { IntlService } from '@island.is/cms-translations' +import type { Locale } from '@island.is/shared/types' +import { isDefined } from '@island.is/shared/utils' +import { Injectable } from '@nestjs/common' +import { PostDefenseChoiceInput } from '../dto/postDefenseChoiceInput.model' +import { ActionTypeEnum } from '../models/actions.model' +import { CourtCase } from '../models/courtCase.model' +import { CourtCases } from '../models/courtCases.model' +import { Item } from '../models/item.model' +import { Lawyers } from '../models/lawyers.model' +import { Subpoena } from '../models/subpoena.model' +import { + DefenseChoices, + mapDefenseChoice, + mapDefenseChoiceForSubpoena, + mapTagTypes, +} from './helpers/mappers' + +const namespaces = ['api.law-and-order'] + +@Injectable() +export class LawAndOrderService { + constructor( + private api: JudicialSystemSPClientService, + private readonly intlService: IntlService, + ) {} + + async getCourtCases(user: User, locale: Locale) { + const cases: Array | null = await this.api.getCases( + user, + locale, + ) + + const data: CourtCases = { + cases: + cases?.map((x: CasesResponse) => { + return { + id: x.id, + caseNumber: x.caseNumber, + caseNumberTitle: x.caseNumber, + state: { label: x.state.label, color: mapTagTypes(x.state.color) }, + type: x.type, + } + }) ?? [], + } + + return data + } + + async getCourtCase(user: User, id: string, locale: Locale) { + const { formatMessage } = await this.intlService.useIntl(namespaces, locale) + const singleCase = await this.api.getCase(id, user, locale) + const hasBeenServed = singleCase?.data.hasBeenServed + + const subpoenaString = formatMessage({ + id: 'api.law-and-order:subpoena', + defaultMessage: 'Fyrirkall', + }) + const seeSubpoenaString = formatMessage({ + id: 'api.law-and-order:see-subpoena', + defaultMessage: 'Sjá fyrirkall', + }) + const seeSubpoenaInMailboxString = formatMessage({ + id: 'api.law-and-order:see-subpoena-in-mailbox', + defaultMessage: 'Sjá fyrirkall í pósthólfi', + }) + const mailboxLink = formatMessage({ + id: 'api.law-and-order:mailbox-link', + defaultMessage: '/postholf', + }) + const subpoenaLink = { + id: 'api.law-and-order:subpoena-link', + defaultMessage: `/log-og-reglur/domsmal/{caseId}/fyrirkall`, + } + + // If the subpoena has not been acknowledged + // add an action to the line including "fyrirkall" to redirect to the digital mailbox or detail page + const subpoenaSentItem: Item | undefined = { + action: { + data: hasBeenServed + ? formatMessage(subpoenaLink, { caseId: singleCase?.caseId }) + : mailboxLink, + title: hasBeenServed ? seeSubpoenaString : seeSubpoenaInMailboxString, + type: hasBeenServed ? ActionTypeEnum.inbox : ActionTypeEnum.url, + }, + } + + const data: CourtCase = { + data: { + id: singleCase?.caseId ?? id, + hasBeenServed: hasBeenServed, + caseNumberTitle: singleCase?.data.caseNumber, + groups: (singleCase?.data.groups ?? []).map((group, groupIndex) => { + return { + items: group.items.map((item) => { + // Adding action to the line including "fyrirkall" + if ( + groupIndex === 0 && + item.label.toLowerCase().includes(subpoenaString.toLowerCase()) + ) { + return { ...item, action: subpoenaSentItem.action } + } + return item + }), + label: group.label, + } + }), + }, + + texts: undefined, + } + return data + } + + async getSubpoena(user: User, id: string, locale: Locale) { + const { formatMessage } = await this.intlService.useIntl(namespaces, locale) + + const subpoena: SubpoenaResponse | undefined | null = + await this.api.getSubpoena(id, user, locale) + + const defenderChoice = subpoena?.defenderInfo.defenderChoice + const message = defenderChoice + ? formatMessage( + DefenseChoices[subpoena?.defenderInfo.defenderChoice].message, + ) + : '' + + const data: Subpoena = { + data: { + id: subpoena?.caseId ?? id, + hasBeenServed: subpoena?.data?.hasBeenServed, + chosenDefender: [message, subpoena?.defenderInfo?.defenderName] + .filter(isDefined) + .join(', '), + defenderChoice: mapDefenseChoiceForSubpoena( + subpoena?.defenderInfo?.defenderChoice, + ), + canEditDefenderChoice: subpoena?.defenderInfo?.canEdit, + groups: subpoena?.data.groups, + courtContactInfo: subpoena?.defenderInfo?.courtContactInfo, + }, + actions: undefined, + texts: { + confirmation: subpoena?.data.alerts?.find( + (alert) => alert.type === AlertMessageTypeEnum.Success, + )?.message, + description: subpoena?.data.subtitle, + }, + } + return data + } + + async getLawyers(user: User, locale: Locale) { + const { formatMessage } = await this.intlService.useIntl(namespaces, locale) + + const answer: Array | undefined | null = + await this.api.getLawyers(user) + const list: Lawyers = { lawyers: [] } + answer?.map((x) => { + list.lawyers?.push({ + title: [x.name, x.practice].filter(isDefined).join(', '), + nationalId: x.nationalId, + }) + }) + + list.choices = Object.entries(DefenseChoices).map(([code, value]) => ({ + id: code, + label: formatMessage(value.message), + })) + + return list + } + + async postDefenseChoice( + user: User, + input: PostDefenseChoiceInput, + locale: Locale, + ) { + if (!input || !input.choice) return null + + return await this.api.patchSubpoena( + { + caseId: input.caseId, + updateSubpoenaDto: { + defenderChoice: mapDefenseChoice(input.choice), + defenderNationalId: input.lawyersNationalId, + }, + locale: locale, + }, + user, + ) + } +} diff --git a/libs/api/domains/law-and-order/src/models/actions.model.ts b/libs/api/domains/law-and-order/src/models/actions.model.ts new file mode 100644 index 000000000000..5dec024ec504 --- /dev/null +++ b/libs/api/domains/law-and-order/src/models/actions.model.ts @@ -0,0 +1,22 @@ +import { Field, ObjectType, registerEnumType } from '@nestjs/graphql' + +export enum ActionTypeEnum { + file = 'file', + url = 'url', + inbox = 'inbox', +} +registerEnumType(ActionTypeEnum, { + name: 'LawAndOrderActionTypeEnum', +}) + +@ObjectType('LawAndOrderAction') +export class Action { + @Field(() => ActionTypeEnum, { nullable: true }) + type?: ActionTypeEnum + + @Field({ nullable: true }) + title?: string + + @Field({ nullable: true }) + data?: string +} diff --git a/libs/api/domains/law-and-order/src/models/courtCase.model.ts b/libs/api/domains/law-and-order/src/models/courtCase.model.ts new file mode 100644 index 000000000000..f52fdad9a7aa --- /dev/null +++ b/libs/api/domains/law-and-order/src/models/courtCase.model.ts @@ -0,0 +1,39 @@ +import { Field, ID, ObjectType } from '@nestjs/graphql' +import { Action } from './actions.model' +import { Group } from './group.model' + +@ObjectType('LawAndOrderCourtCaseText') +export class Text { + @Field({ nullable: true }) + intro?: string + + @Field({ nullable: true }) + footnote?: string +} + +@ObjectType('LawAndOrderCourtCaseData') +export class Data { + @Field(() => ID) + id!: string + + @Field({ nullable: true }) + caseNumberTitle?: string + + @Field({ nullable: true }) + hasBeenServed?: boolean + + @Field(() => [Group], { nullable: true }) + groups?: Array +} + +@ObjectType('LawAndOrderCourtCase') +export class CourtCase { + @Field(() => Text, { nullable: true }) + texts?: Text + + @Field(() => [Action], { nullable: true }) + actions?: Array + + @Field(() => Data, { nullable: true }) + data?: Data +} diff --git a/libs/api/domains/law-and-order/src/models/courtCases.model.ts b/libs/api/domains/law-and-order/src/models/courtCases.model.ts new file mode 100644 index 000000000000..308cae106981 --- /dev/null +++ b/libs/api/domains/law-and-order/src/models/courtCases.model.ts @@ -0,0 +1,49 @@ +import { Field, ID, ObjectType, registerEnumType } from '@nestjs/graphql' + +export enum CourtCaseStateTagColorEnum { + blue = 'blue', + darkerBlue = 'darkerBlue', + purple = 'purple', + white = 'white', + red = 'red', + rose = 'rose', + blueberry = 'blueberry', + dark = 'dark', + mint = 'mint', + yellow = 'yellow', + disabled = 'disabled', + warn = 'warn', +} +registerEnumType(CourtCaseStateTagColorEnum, { + name: 'LawAndOrderCourtCaseStateTagColorEnum', +}) + +@ObjectType('LawAndOrderCourtCasesState') +export class State { + @Field({ nullable: true }) + label?: string + + @Field(() => CourtCaseStateTagColorEnum, { nullable: true }) + color?: CourtCaseStateTagColorEnum +} + +@ObjectType('LawAndOrderCourtCasesCase') +export class Case { + @Field(() => ID) + id!: string + + @Field({ nullable: true }) + caseNumberTitle?: string + + @Field({ nullable: true }) + type?: string + + @Field(() => State, { nullable: true }) + state?: State +} + +@ObjectType('LawAndOrderCourtCases') +export class CourtCases { + @Field(() => [Case], { nullable: true }) + cases?: Array +} diff --git a/libs/api/domains/law-and-order/src/models/defenseChoice.model.ts b/libs/api/domains/law-and-order/src/models/defenseChoice.model.ts new file mode 100644 index 000000000000..80d1b2cf39fc --- /dev/null +++ b/libs/api/domains/law-and-order/src/models/defenseChoice.model.ts @@ -0,0 +1,14 @@ +import { Field, ObjectType } from '@nestjs/graphql' +import { DefenseChoiceEnum } from './defenseChoiceEnum.model' + +@ObjectType('LawAndOrderDefenseChoice') +export class DefenseChoice { + @Field() + caseId!: string + + @Field(() => DefenseChoice, { nullable: true }) + choice?: DefenseChoiceEnum + + @Field({ nullable: true }) + lawyersNationalId?: string +} diff --git a/libs/api/domains/law-and-order/src/models/defenseChoiceEnum.model.ts b/libs/api/domains/law-and-order/src/models/defenseChoiceEnum.model.ts new file mode 100644 index 000000000000..e4a4297668da --- /dev/null +++ b/libs/api/domains/law-and-order/src/models/defenseChoiceEnum.model.ts @@ -0,0 +1,11 @@ +import { registerEnumType } from '@nestjs/graphql' + +export enum DefenseChoiceEnum { + WAIVE = 'WAIVE', + CHOOSE = 'CHOOSE', + DELAY = 'DELAY', + DELEGATE = 'DELEGATE', +} +registerEnumType(DefenseChoiceEnum, { + name: 'LawAndOrderDefenseChoiceEnum', +}) diff --git a/libs/api/domains/law-and-order/src/models/group.model.ts b/libs/api/domains/law-and-order/src/models/group.model.ts new file mode 100644 index 000000000000..4020ee9d8bc1 --- /dev/null +++ b/libs/api/domains/law-and-order/src/models/group.model.ts @@ -0,0 +1,11 @@ +import { Field, ObjectType } from '@nestjs/graphql' +import { Item } from './item.model' + +@ObjectType('LawAndOrderGroup') +export class Group { + @Field({ nullable: true }) + label?: string + + @Field(() => [Item], { nullable: true }) + items?: Array +} diff --git a/libs/api/domains/law-and-order/src/models/item.model.ts b/libs/api/domains/law-and-order/src/models/item.model.ts new file mode 100644 index 000000000000..3e2b6ca2d067 --- /dev/null +++ b/libs/api/domains/law-and-order/src/models/item.model.ts @@ -0,0 +1,17 @@ +import { Field, ObjectType } from '@nestjs/graphql' +import { Action } from './actions.model' + +@ObjectType('LawAndOrderSubpoenaItem') +export class Item { + @Field({ nullable: true }) + label?: string + + @Field({ nullable: true }) + value?: string + + @Field({ nullable: true }) + link?: string + + @Field(() => Action, { nullable: true }) + action?: Action +} diff --git a/libs/api/domains/law-and-order/src/models/lawyers.model.ts b/libs/api/domains/law-and-order/src/models/lawyers.model.ts new file mode 100644 index 000000000000..d6e1977100eb --- /dev/null +++ b/libs/api/domains/law-and-order/src/models/lawyers.model.ts @@ -0,0 +1,28 @@ +import { Field, ObjectType } from '@nestjs/graphql' + +@ObjectType('LawAndOrderLawyerChoices') +export class Choice { + @Field({ nullable: true }) + id?: string + + @Field({ nullable: true }) + label?: string +} + +@ObjectType('LawAndOrderLawyer') +export class Lawyer { + @Field({ nullable: true }) + title?: string + + @Field({ nullable: true }) + nationalId?: string +} + +@ObjectType('LawAndOrderLawyers') +export class Lawyers { + @Field(() => [Lawyer], { nullable: true }) + lawyers?: Array + + @Field(() => [Choice], { nullable: true }) + choices?: Array +} diff --git a/libs/api/domains/law-and-order/src/models/subpoena.model.ts b/libs/api/domains/law-and-order/src/models/subpoena.model.ts new file mode 100644 index 000000000000..62a46c4b61ed --- /dev/null +++ b/libs/api/domains/law-and-order/src/models/subpoena.model.ts @@ -0,0 +1,66 @@ +import { Field, ID, ObjectType } from '@nestjs/graphql' +import { Action } from './actions.model' +import { DefenseChoiceEnum } from './defenseChoiceEnum.model' +import { Group } from './group.model' + +@ObjectType('LawAndOrderSubpoenaAlert') +export class Alert { + @Field({ nullable: true }) + type?: string + + @Field({ nullable: true }) + message?: string +} +@ObjectType('LawAndOrderSubpoenaTexts') +export class Text { + @Field({ nullable: true }) + intro?: string + + @Field({ nullable: true }) + confirmation?: string + + @Field({ nullable: true }) + description?: string + + @Field({ nullable: true }) + claim?: string +} + +@ObjectType('LawAndOrderSubpoenaData') +export class Data { + @Field(() => ID) + id!: string + + @Field({ nullable: true }) + hasBeenServed?: boolean + + @Field({ nullable: true }) + chosenDefender?: string + + @Field(() => Boolean, { nullable: true }) + canEditDefenderChoice?: boolean + + @Field({ nullable: true }) + courtContactInfo?: string + + @Field(() => DefenseChoiceEnum, { nullable: true }) + defenderChoice?: DefenseChoiceEnum + + @Field(() => [Group], { nullable: true }) + groups?: Array + + @Field(() => [Alert], { nullable: true }) + alerts?: Array +} + +@ObjectType('LawAndOrderSubpoena') +export class Subpoena { + @Field({ nullable: true }) + texts?: Text + + @Field(() => [Action], { nullable: true }) + actions?: Array + + @Field(() => Data, { nullable: true }) + data?: Data +} diff --git a/libs/api/domains/law-and-order/tsconfig.json b/libs/api/domains/law-and-order/tsconfig.json new file mode 100644 index 000000000000..2c9d3a5ecbc9 --- /dev/null +++ b/libs/api/domains/law-and-order/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs" + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/api/domains/law-and-order/tsconfig.lib.json b/libs/api/domains/law-and-order/tsconfig.lib.json new file mode 100644 index 000000000000..28369ef7622b --- /dev/null +++ b/libs/api/domains/law-and-order/tsconfig.lib.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "outDir": "../../../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"], + "include": ["src/**/*.ts"] +} diff --git a/libs/api/domains/law-and-order/tsconfig.spec.json b/libs/api/domains/law-and-order/tsconfig.spec.json new file mode 100644 index 000000000000..6668655fc397 --- /dev/null +++ b/libs/api/domains/law-and-order/tsconfig.spec.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/libs/auth/scopes/src/lib/api.scope.ts b/libs/auth/scopes/src/lib/api.scope.ts index 68f0cfdcbbff..b21d714c0228 100644 --- a/libs/auth/scopes/src/lib/api.scope.ts +++ b/libs/auth/scopes/src/lib/api.scope.ts @@ -10,6 +10,7 @@ export enum ApiScope { internal = '@island.is/internal', internalProcuring = '@island.is/internal:procuring', meDetails = '@island.is/me:details', + lawAndOrder = '@island.is/law-and-order', licenses = '@island.is/licenses', licensesVerify = '@island.is/licenses:verify', company = '@island.is/company', diff --git a/libs/clients/documents-v2/src/clientConfig.json b/libs/clients/documents-v2/src/clientConfig.json index 1eb13624a7cc..e1b0bcb38848 100644 --- a/libs/clients/documents-v2/src/clientConfig.json +++ b/libs/clients/documents-v2/src/clientConfig.json @@ -132,7 +132,14 @@ "in": "query", "required": false, "type": "string", - "enum": ["Date", "Category", "Type", "Sender", "Subject"] + "enum": [ + "Date", + "Category", + "Type", + "Sender", + "Subject", + "Publication" + ] }, { "name": "order", @@ -390,6 +397,18 @@ "in": "query", "required": true, "type": "string" + }, + { + "name": "includeDocument", + "in": "query", + "required": false, + "type": "boolean" + }, + { + "name": "locale", + "in": "query", + "required": false, + "type": "string" } ], "responses": { @@ -520,7 +539,14 @@ "in": "query", "required": false, "type": "string", - "enum": ["Date", "Category", "Type", "Sender", "Subject"] + "enum": [ + "Date", + "Category", + "Type", + "Sender", + "Subject", + "Publication" + ] }, { "name": "order", @@ -563,6 +589,28 @@ } } } + }, + "/api/mail/v1/customers/{kennitala}/messages/unreadCount": { + "get": { + "tags": ["Customers"], + "operationId": "Customers_GetUnreadCount", + "consumes": [], + "produces": ["application/json", "text/json"], + "parameters": [ + { + "name": "kennitala", + "in": "path", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { "format": "int32", "type": "integer" } + } + } + } } }, "definitions": { @@ -633,7 +681,9 @@ "withdrawn": { "type": "boolean" }, "withdrawnReason": { "type": "string" }, "minumumAuthenticationType": { "type": "string" }, - "bookmarked": { "type": "boolean" } + "bookmarked": { "type": "boolean" }, + "fileType": { "type": "string" }, + "urgent": { "type": "boolean" } } }, "BatchRequest": { @@ -677,7 +727,20 @@ "senderName": { "type": "string" }, "senderKennitala": { "type": "string" }, "subject": { "type": "string" }, - "categoryId": { "format": "int32", "type": "integer" } + "categoryId": { "format": "int32", "type": "integer" }, + "urgent": { "type": "boolean" }, + "actions": { + "type": "array", + "items": { "$ref": "#/definitions/MessageAction" } + } + } + }, + "MessageAction": { + "type": "object", + "properties": { + "type": { "type": "string" }, + "title": { "type": "string" }, + "data": { "type": "string" } } }, "PaperMailDTO": { diff --git a/libs/clients/documents-v2/src/lib/documentsClientV2.service.ts b/libs/clients/documents-v2/src/lib/documentsClientV2.service.ts index 4381e3457c89..f718345c4df1 100644 --- a/libs/clients/documents-v2/src/lib/documentsClientV2.service.ts +++ b/libs/clients/documents-v2/src/lib/documentsClientV2.service.ts @@ -78,14 +78,18 @@ export class DocumentsClientV2Service { async getCustomersDocument( customerId: string, documentId: string, + locale?: string, + includeDocument?: boolean, ): Promise { const document = await this.api.customersDocument({ kennitala: customerId, messageId: documentId, authenticationType: 'HIGH', + locale: locale, + includeDocument: includeDocument, }) - const mappedDocument = mapToDocument(document) + const mappedDocument = mapToDocument(document, includeDocument) if (!mappedDocument) { this.logger.warn('No document content available for findDocumentById', { diff --git a/libs/clients/documents-v2/src/lib/dto/document.dto.ts b/libs/clients/documents-v2/src/lib/dto/document.dto.ts index 3b99498f011d..7a9dd61643eb 100644 --- a/libs/clients/documents-v2/src/lib/dto/document.dto.ts +++ b/libs/clients/documents-v2/src/lib/dto/document.dto.ts @@ -1,5 +1,5 @@ +import { DocumentDTO, MessageAction } from '../..' import sanitizeHtml from 'sanitize-html' -import { DocumentDTO } from '../..' const customDocument = { senderName: 'Ríkisskattstjóri', @@ -13,7 +13,7 @@ export type FileType = 'pdf' | 'html' | 'url' export type DocumentDto = { fileName?: string fileType: FileType - content: string + content?: string | null date?: Date bookmarked?: boolean archived?: boolean @@ -21,10 +21,27 @@ export type DocumentDto = { senderNationalId?: string subject: string categoryId?: string + urgent?: boolean + actions?: Array } -export const mapToDocument = (document: DocumentDTO): DocumentDto | null => { +export const mapToDocument = ( + document: DocumentDTO, + includeDocument?: boolean, +): DocumentDto | null => { let fileType: FileType, content: string + const returnData = { + fileName: document.fileName, + date: document.publicationDate, + bookmarked: document.bookmarked, + archived: document.archived, + senderName: document.senderName, + senderNationalId: document.senderKennitala, + subject: document.subject ?? 'Óþekktur titill', // All of the content in this service is strictly Icelandic. Fallback to match. + categoryId: document.categoryId?.toString(), + urgent: document.urgent, + actions: document.actions, + } if (document.content) { fileType = 'pdf' content = document.content @@ -55,20 +72,14 @@ export const mapToDocument = (document: DocumentDTO): DocumentDto | null => { ]), }) content = html + } else if (!includeDocument) { + return { ...returnData, content: null, fileType: 'pdf' } } else { return null } - return { - fileName: document.fileName, - fileType: fileType, - content: content, - date: document.publicationDate, - bookmarked: document.bookmarked, - archived: document.archived, - senderName: document.senderName, - senderNationalId: document.senderKennitala, - subject: document.subject ?? 'Óþekktur titill', // All of the content in this service is strictly Icelandic. Fallback to match. - categoryId: document.categoryId?.toString(), + ...returnData, + content, + fileType, } } diff --git a/libs/clients/documents-v2/src/lib/dto/documentInfo.dto.ts b/libs/clients/documents-v2/src/lib/dto/documentInfo.dto.ts index 43571d89519a..a93313ead3d6 100644 --- a/libs/clients/documents-v2/src/lib/dto/documentInfo.dto.ts +++ b/libs/clients/documents-v2/src/lib/dto/documentInfo.dto.ts @@ -11,6 +11,7 @@ export interface DocumentInfoDto withdrawn?: boolean widthdrawnReason?: string minimumAuthenticationType?: string + urgent?: boolean } export const mapToDocumentInfoDto = ( diff --git a/libs/clients/judicial-system-sp/.eslintrc.json b/libs/clients/judicial-system-sp/.eslintrc.json new file mode 100644 index 000000000000..3456be9b9036 --- /dev/null +++ b/libs/clients/judicial-system-sp/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/clients/judicial-system-sp/README.md b/libs/clients/judicial-system-sp/README.md new file mode 100644 index 000000000000..9146256384c8 --- /dev/null +++ b/libs/clients/judicial-system-sp/README.md @@ -0,0 +1,9 @@ +# Judicial System client + +## Service Portal + +This client's main focus is data for the Service Portal, Law and Order module. Displaying and receiving information about court cases and data related to it, subpoenas, lawyers etc. + +## Judicial System App + +This client is not related to the app itself "Judicial System" although coming from the same organization, Dómsmálaráðuneytið. diff --git a/libs/clients/judicial-system-sp/jest.config.ts b/libs/clients/judicial-system-sp/jest.config.ts new file mode 100644 index 000000000000..0cca6ecae497 --- /dev/null +++ b/libs/clients/judicial-system-sp/jest.config.ts @@ -0,0 +1,11 @@ +/* eslint-disable */ +export default { + displayName: 'clients-judicial-system-sp', + preset: '../../../jest.preset.js', + testEnvironment: 'node', + transform: { + '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../../coverage/libs/clients/judicial-system-sp', +} diff --git a/libs/clients/judicial-system-sp/project.json b/libs/clients/judicial-system-sp/project.json new file mode 100644 index 000000000000..e8a34f0c5097 --- /dev/null +++ b/libs/clients/judicial-system-sp/project.json @@ -0,0 +1,48 @@ +{ + "name": "clients-judicial-system-sp", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/clients/judicial-system-sp/src", + "projectType": "library", + "targets": { + "lint": { + "executor": "@nx/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["libs/clients/judicial-system-sp/**/*.ts"] + } + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/clients/judicial-system-sp/jest.config.ts", + "passWithNoTests": true + }, + "configurations": { + "ci": { + "ci": true, + "codeCoverage": true + } + } + }, + "update-openapi-document": { + "executor": "nx:run-commands", + "options": { + "commands": [ + "curl -H \"X-Road-Client: IS-DEV/GOV/10000/island-is-client\" http://localhost:8081/r1/IS-DEV/GOV/10014/Rettarvorslugatt-Private/getOpenAPI?serviceCode=judicial-system-mailbox-api -H 'Accept: application/json' > src/clientConfig.json", + "prettier --write src/clientConfig.json" + ], + "parallel": false, + "cwd": "libs/clients/judicial-system-sp" + } + }, + "codegen/backend-client": { + "executor": "nx:run-commands", + "options": { + "command": "yarn openapi-generator -o libs/clients/judicial-system-sp/gen/fetch -i libs/clients/judicial-system-sp/src/clientConfig.json" + }, + "outputs": ["{projectRoot}/gen/fetch"] + } + }, + "tags": ["lib:client", "scope:client"] +} diff --git a/libs/clients/judicial-system-sp/src/clientConfig.json b/libs/clients/judicial-system-sp/src/clientConfig.json new file mode 100644 index 000000000000..746792eb0b07 --- /dev/null +++ b/libs/clients/judicial-system-sp/src/clientConfig.json @@ -0,0 +1,515 @@ +{ + "openapi": "3.0.0", + "paths": { + "/liveness": { + "get": { + "operationId": "InfraController_liveness", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/Liveness" } + } + } + } + }, + "tags": ["internal"] + } + }, + "/version": { + "get": { + "operationId": "InfraController_version", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/Version" } + } + } + } + }, + "tags": ["internal"] + } + }, + "/api/cases": { + "get": { + "operationId": "CaseController_getAllCases", + "parameters": [ + { + "name": "locale", + "required": false, + "in": "query", + "description": "The requested locale of the response. Defaults to Icelandic.", + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "Returns a list of accessible indictment cases for authenticated user. If user has no cases it returns an empty list.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { "$ref": "#/components/schemas/CasesResponse" } + } + } + } + }, + "400": { "description": "Bad Request" }, + "401": { + "description": "User is not authorized to perform this action" + }, + "500": { "description": "Internal Server Error" } + }, + "tags": ["cases"] + } + }, + "/api/case/{caseId}": { + "get": { + "operationId": "CaseController_getCase", + "parameters": [ + { + "name": "caseId", + "required": true, + "in": "path", + "schema": { "type": "string" } + }, + { + "name": "locale", + "required": false, + "in": "query", + "description": "The requested locale of the response. Defaults to Icelandic.", + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "Returns indictment case by case id", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/CaseResponse" } + } + } + }, + "400": { "description": "Bad Request" }, + "401": { + "description": "User is not authorized to perform this action" + }, + "404": { + "description": "Case for given case id and authenticated user not found" + }, + "500": { "description": "Internal Server Error" } + }, + "tags": ["cases"] + } + }, + "/api/case/{caseId}/subpoena": { + "get": { + "operationId": "CaseController_getSubpoena", + "parameters": [ + { + "name": "caseId", + "required": true, + "in": "path", + "schema": { "type": "string" } + }, + { + "name": "locale", + "required": false, + "in": "query", + "description": "The requested locale of the response. Defaults to Icelandic.", + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "Returns subpoena by case id", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/SubpoenaResponse" } + } + } + }, + "400": { "description": "Bad Request" }, + "401": { + "description": "User is not authorized to perform this action" + }, + "404": { + "description": "Subpoena for given case id and authenticated user not found" + }, + "500": { "description": "Internal Server Error" } + }, + "tags": ["cases"] + }, + "patch": { + "operationId": "CaseController_updateSubpoena", + "parameters": [ + { + "name": "caseId", + "required": true, + "in": "path", + "schema": { "type": "string" } + }, + { + "name": "locale", + "required": false, + "in": "query", + "description": "The requested locale of the response. Defaults to Icelandic.", + "schema": { "type": "string" } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/UpdateSubpoenaDto" } + } + } + }, + "responses": { + "200": { + "description": "Updates subpoena info", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/SubpoenaResponse" } + } + } + }, + "400": { "description": "Bad Request" }, + "401": { + "description": "User is not authorized to perform this action" + }, + "403": { "description": "User is not allowed to update subpoena" }, + "404": { + "description": "Subpoena for given case id and authenticated user not found" + }, + "500": { "description": "Internal Server Error" } + }, + "tags": ["cases"] + } + }, + "/api/defenders": { + "get": { + "operationId": "DefenderController_getLawyers", + "parameters": [], + "responses": { + "200": { + "description": "Returns a list of defenders", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { "$ref": "#/components/schemas/Defender" } + } + } + } + }, + "500": { "description": "Failed to retrieve defenders" } + }, + "tags": ["defenders"] + } + }, + "/api/defender/{nationalId}": { + "get": { + "operationId": "DefenderController_getLawyer", + "parameters": [ + { + "name": "nationalId", + "required": true, + "in": "path", + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "Retrieves a defender by national id", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/Defender" } + } + } + } + }, + "tags": ["defenders"] + } + }, + "/health/check": { + "get": { + "operationId": "HealthController_check", + "parameters": [], + "responses": { + "200": { + "description": "The Health Check is successful", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { "type": "string", "example": "ok" }, + "info": { + "type": "object", + "example": { "database": { "status": "up" } }, + "additionalProperties": { + "type": "object", + "properties": { "status": { "type": "string" } }, + "additionalProperties": { "type": "string" } + }, + "nullable": true + }, + "error": { + "type": "object", + "example": {}, + "additionalProperties": { + "type": "object", + "properties": { "status": { "type": "string" } }, + "additionalProperties": { "type": "string" } + }, + "nullable": true + }, + "details": { + "type": "object", + "example": { "database": { "status": "up" } }, + "additionalProperties": { + "type": "object", + "properties": { "status": { "type": "string" } }, + "additionalProperties": { "type": "string" } + } + } + } + } + } + } + }, + "503": { + "description": "The Health Check is not successful", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { "type": "string", "example": "error" }, + "info": { + "type": "object", + "example": { "database": { "status": "up" } }, + "additionalProperties": { + "type": "object", + "properties": { "status": { "type": "string" } }, + "additionalProperties": { "type": "string" } + }, + "nullable": true + }, + "error": { + "type": "object", + "example": { + "redis": { + "status": "down", + "message": "Could not connect" + } + }, + "additionalProperties": { + "type": "object", + "properties": { "status": { "type": "string" } }, + "additionalProperties": { "type": "string" } + }, + "nullable": true + }, + "details": { + "type": "object", + "example": { + "database": { "status": "up" }, + "redis": { + "status": "down", + "message": "Could not connect" + } + }, + "additionalProperties": { + "type": "object", + "properties": { "status": { "type": "string" } }, + "additionalProperties": { "type": "string" } + } + } + } + } + } + } + } + } + } + } + }, + "info": { + "title": "Judicial System xRoad robot API", + "description": "This is the xRoad robot API for the judicial system.", + "version": "1.0", + "contact": {} + }, + "tags": [{ "name": "judicial-system", "description": "" }], + "servers": [], + "components": { + "schemas": { + "Liveness": { + "type": "object", + "properties": { "ok": { "type": "boolean" } }, + "required": ["ok"] + }, + "Version": { + "type": "object", + "properties": { "version": { "type": "string" } }, + "required": ["version"] + }, + "StateTag": { + "type": "object", + "properties": { + "color": { + "type": "string", + "enum": [ + "blue", + "darkerBlue", + "purple", + "white", + "red", + "rose", + "blueberry", + "dark", + "mint", + "yellow", + "disabled", + "warn" + ] + }, + "label": { "type": "string" } + }, + "required": ["color", "label"] + }, + "CasesResponse": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "caseNumber": { "type": "string" }, + "type": { "type": "string" }, + "state": { "$ref": "#/components/schemas/StateTag" } + }, + "required": ["id", "caseNumber", "type", "state"] + }, + "Items": { + "type": "object", + "properties": { + "label": { "type": "string" }, + "value": { "type": "string" }, + "linkType": { "type": "string", "enum": ["email", "tel"] } + }, + "required": ["label", "value", "linkType"] + }, + "Groups": { + "type": "object", + "properties": { + "label": { "type": "string" }, + "items": { + "type": "array", + "items": { "$ref": "#/components/schemas/Items" } + } + }, + "required": ["label", "items"] + }, + "IndictmentCaseData": { + "type": "object", + "properties": { + "caseNumber": { "type": "string" }, + "hasBeenServed": { "type": "boolean" }, + "groups": { + "type": "array", + "items": { "$ref": "#/components/schemas/Groups" } + } + }, + "required": ["caseNumber", "hasBeenServed", "groups"] + }, + "CaseResponse": { + "type": "object", + "properties": { + "caseId": { "type": "string" }, + "data": { "$ref": "#/components/schemas/IndictmentCaseData" } + }, + "required": ["caseId", "data"] + }, + "AlertMessage": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["error", "info", "success", "warning", "default"] + }, + "message": { "type": "string" } + }, + "required": ["type", "message"] + }, + "SubpoenaData": { + "type": "object", + "properties": { + "title": { "type": "string" }, + "subtitle": { "type": "string" }, + "groups": { + "type": "array", + "items": { "$ref": "#/components/schemas/Groups" } + }, + "alerts": { + "type": "array", + "items": { "$ref": "#/components/schemas/AlertMessage" } + }, + "hasBeenServed": { "type": "boolean" } + }, + "required": ["title", "subtitle", "groups", "alerts", "hasBeenServed"] + }, + "DefenderInfo": { + "type": "object", + "properties": { + "defenderChoice": { + "type": "string", + "enum": ["WAIVE", "CHOOSE", "DELAY", "DELEGATE"] + }, + "defenderName": { "type": "string" }, + "canEdit": { "type": "boolean" }, + "courtContactInfo": { "type": "string" } + }, + "required": [ + "defenderChoice", + "defenderName", + "canEdit", + "courtContactInfo" + ] + }, + "SubpoenaResponse": { + "type": "object", + "properties": { + "caseId": { "type": "string" }, + "data": { "$ref": "#/components/schemas/SubpoenaData" }, + "defenderInfo": { "$ref": "#/components/schemas/DefenderInfo" } + }, + "required": ["caseId", "data", "defenderInfo"] + }, + "UpdateSubpoenaDto": { + "type": "object", + "properties": { + "defenderChoice": { + "type": "string", + "enum": ["WAIVE", "CHOOSE", "DELAY", "DELEGATE"] + }, + "defenderNationalId": { "type": "string" } + }, + "required": ["defenderChoice"] + }, + "Defender": { + "type": "object", + "properties": { + "nationalId": { "type": "string" }, + "name": { "type": "string" }, + "practice": { "type": "string" } + }, + "required": ["nationalId", "name", "practice"] + } + } + } +} diff --git a/libs/clients/judicial-system-sp/src/index.ts b/libs/clients/judicial-system-sp/src/index.ts new file mode 100644 index 000000000000..aa335ced6de2 --- /dev/null +++ b/libs/clients/judicial-system-sp/src/index.ts @@ -0,0 +1,9 @@ +export { JudicialSystemSPClientConfig } from './lib/judicialSystemSPClient.config' +export { JudicialSystemSPClientModule } from './lib/judicialSystemSPClient.module' +export { + CasesApi, + DefendersApi, + UpdateSubpoenaDtoDefenderChoiceEnum, +} from '../gen/fetch' +export { JudicialSystemSPClientService } from './lib/judicialSystemSPClient.service' +export * from '../gen/fetch/models' diff --git a/libs/clients/judicial-system-sp/src/lib/judicialSystemSPClient.config.ts b/libs/clients/judicial-system-sp/src/lib/judicialSystemSPClient.config.ts new file mode 100644 index 000000000000..f5b11a96cfeb --- /dev/null +++ b/libs/clients/judicial-system-sp/src/lib/judicialSystemSPClient.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from '@island.is/nest/config' +import { z } from 'zod' + +const schema = z.object({ + xRoadServicePath: z.string(), + scope: z.array(z.string()), +}) + +export const JudicialSystemSPClientConfig = defineConfig< + z.infer +>({ + name: 'JudicialSystemSPClient', + schema, + load(env) { + return { + xRoadServicePath: env.required( + 'XROAD_JUDICIAL_SYSTEM_SP_PATH', + 'IS-DEV/GOV/10014/Rettarvorslugatt-Private/judicial-system-mailbox-api', + ), + scope: ['@rettarvorslugatt.island.is/law-and-order'], + } + }, +}) diff --git a/libs/clients/judicial-system-sp/src/lib/judicialSystemSPClient.module.ts b/libs/clients/judicial-system-sp/src/lib/judicialSystemSPClient.module.ts new file mode 100644 index 000000000000..8c3272508680 --- /dev/null +++ b/libs/clients/judicial-system-sp/src/lib/judicialSystemSPClient.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common' +import { SharedApiConfig } from './shared.config' +import { exportedApis } from './providers' +import { JudicialSystemSPClientService } from './judicialSystemSPClient.service' + +@Module({ + providers: [SharedApiConfig, ...exportedApis, JudicialSystemSPClientService], + exports: [JudicialSystemSPClientService], +}) +export class JudicialSystemSPClientModule {} diff --git a/libs/clients/judicial-system-sp/src/lib/judicialSystemSPClient.service.ts b/libs/clients/judicial-system-sp/src/lib/judicialSystemSPClient.service.ts new file mode 100644 index 000000000000..e596bbae051b --- /dev/null +++ b/libs/clients/judicial-system-sp/src/lib/judicialSystemSPClient.service.ts @@ -0,0 +1,63 @@ +import { LOGGER_PROVIDER } from '@island.is/logging' +import type { Logger } from '@island.is/logging' +import { Inject, Injectable } from '@nestjs/common' +import { + CaseControllerUpdateSubpoenaRequest, + CasesApi, + DefendersApi, +} from '../../gen/fetch' +import { Auth, AuthMiddleware, User } from '@island.is/auth-nest-tools' +import { handle404 } from '@island.is/clients/middlewares' + +@Injectable() +export class JudicialSystemSPClientService { + constructor( + @Inject(LOGGER_PROVIDER) private readonly logger: Logger, + private casesApi: CasesApi, + private defendersApi: DefendersApi, + ) {} + + private casesApiWithAuth = (user: User) => + this.casesApi.withMiddleware(new AuthMiddleware(user as Auth)) + + private defenderApiWithAuth = (user: User) => + this.defendersApi.withMiddleware(new AuthMiddleware(user as Auth)) + + async getCases(user: User, locale: string) { + return this.casesApiWithAuth(user) + .caseControllerGetAllCases({ + locale: locale, + }) + .catch(handle404) + } + + async getCase(id: string, user: User, locale: string) { + return this.casesApiWithAuth(user) + .caseControllerGetCase({ + caseId: id, + locale: locale, + }) + .catch(handle404) + } + + async getLawyers(user: User) { + return this.defenderApiWithAuth(user) + .defenderControllerGetLawyers() + .catch(handle404) + } + + async getSubpoena(id: string, user: User, locale: string) { + return this.casesApiWithAuth(user) + .caseControllerGetSubpoena({ + caseId: id, + locale: locale, + }) + .catch(handle404) + } + + async patchSubpoena(input: CaseControllerUpdateSubpoenaRequest, user: User) { + return this.casesApiWithAuth(user) + .caseControllerUpdateSubpoena(input) + .catch(handle404) + } +} diff --git a/libs/clients/judicial-system-sp/src/lib/providers.ts b/libs/clients/judicial-system-sp/src/lib/providers.ts new file mode 100644 index 000000000000..7c2eaac8c61a --- /dev/null +++ b/libs/clients/judicial-system-sp/src/lib/providers.ts @@ -0,0 +1,10 @@ +import { CasesApi, Configuration, DefendersApi } from '../../gen/fetch' +import { SharedApiConfig } from './shared.config' + +export const exportedApis = [CasesApi, DefendersApi].map((Api) => ({ + provide: Api, + useFactory: (configuration: Configuration) => { + return new Api(configuration) + }, + inject: [SharedApiConfig.provide], +})) diff --git a/libs/clients/judicial-system-sp/src/lib/shared.config.ts b/libs/clients/judicial-system-sp/src/lib/shared.config.ts new file mode 100644 index 000000000000..3b5b72d5c5b9 --- /dev/null +++ b/libs/clients/judicial-system-sp/src/lib/shared.config.ts @@ -0,0 +1,45 @@ +import { createEnhancedFetch } from '@island.is/clients/middlewares' +import { + ConfigType, + IdsClientConfig, + LazyDuringDevScope, + XRoadConfig, +} from '@island.is/nest/config' +import { Configuration } from '../../gen/fetch' +import { JudicialSystemSPClientConfig } from './judicialSystemSPClient.config' + +export const SharedApiConfig = { + provide: 'JudicialSystemSPApiProviderConfiguration', + scope: LazyDuringDevScope, + useFactory: ( + xroadConfig: ConfigType, + config: ConfigType, + idsClientConfig: ConfigType, + ) => + new Configuration({ + fetchApi: createEnhancedFetch({ + name: 'clients-judicial-system-sp', + organizationSlug: 'domsmalaraduneytid', + logErrorResponseBody: true, + autoAuth: idsClientConfig.isConfigured + ? { + mode: 'tokenExchange', + issuer: idsClientConfig.issuer, + clientId: idsClientConfig.clientId, + clientSecret: idsClientConfig.clientSecret, + scope: config.scope, + } + : undefined, + }), + basePath: `${xroadConfig.xRoadBasePath}/r1/${config.xRoadServicePath}`, + headers: { + 'X-Road-Client': xroadConfig.xRoadClient, + Accept: 'application/json', + }, + }), + inject: [ + XRoadConfig.KEY, + JudicialSystemSPClientConfig.KEY, + IdsClientConfig.KEY, + ], +} diff --git a/libs/clients/judicial-system-sp/tsconfig.json b/libs/clients/judicial-system-sp/tsconfig.json new file mode 100644 index 000000000000..25f7201d870e --- /dev/null +++ b/libs/clients/judicial-system-sp/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs" + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/clients/judicial-system-sp/tsconfig.lib.json b/libs/clients/judicial-system-sp/tsconfig.lib.json new file mode 100644 index 000000000000..e583571eac87 --- /dev/null +++ b/libs/clients/judicial-system-sp/tsconfig.lib.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "outDir": "../../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"], + "include": ["src/**/*.ts"] +} diff --git a/libs/clients/judicial-system-sp/tsconfig.spec.json b/libs/clients/judicial-system-sp/tsconfig.spec.json new file mode 100644 index 000000000000..69a251f328ce --- /dev/null +++ b/libs/clients/judicial-system-sp/tsconfig.spec.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/libs/feature-flags/src/lib/features.ts b/libs/feature-flags/src/lib/features.ts index acefa400d386..0749442f1775 100644 --- a/libs/feature-flags/src/lib/features.ts +++ b/libs/feature-flags/src/lib/features.ts @@ -55,6 +55,8 @@ export enum Features { servicePortalSocialInsuranceIncomePlanPageEnabled = 'isServicePortalSocialInsuranceIncomePlanPageEnabled', ServicePortalNotificationsEnabled = 'isServicePortalNotificationsPageEnabled', + servicePortalLawAndOrderModuleEnabled = 'isServicePortalLawAndOrderModuleEnabled', + servicePortalDocumentsActionsEnabled = 'isServicePortalDocumentsActionsEnabled', //Occupational License Health directorate fetch enabled occupationalLicensesHealthDirectorate = 'isHealthDirectorateOccupationalLicenseEnabled', diff --git a/libs/service-portal/assets/src/screens/VehicleDetail/VehicleDetail.tsx b/libs/service-portal/assets/src/screens/VehicleDetail/VehicleDetail.tsx index 1ade3d2b929d..d85a8a573372 100644 --- a/libs/service-portal/assets/src/screens/VehicleDetail/VehicleDetail.tsx +++ b/libs/service-portal/assets/src/screens/VehicleDetail/VehicleDetail.tsx @@ -27,6 +27,7 @@ import { IntroHeader, SAMGONGUSTOFA_SLUG, LinkButton, + getDateLocale, } from '@island.is/service-portal/core' import OwnersTable from '../../components/DetailTable/OwnersTable' @@ -44,7 +45,6 @@ import { import { displayWithUnit } from '../../utils/displayWithUnit' import AxleTable from '../../components/DetailTable/AxleTable' import Dropdown from '../../components/Dropdown/Dropdown' -import { getDateLocale } from '../../utils/constants' import { useGetUsersVehiclesDetailQuery } from './VehicleDetail.generated' import { AssetsPaths } from '../../lib/paths' import { useFeatureFlagClient } from '@island.is/react/feature-flags' diff --git a/libs/service-portal/assets/src/screens/VehicleHistory/HistoryTableData.tsx b/libs/service-portal/assets/src/screens/VehicleHistory/HistoryTableData.tsx index 1a979d141f81..3871f2b45cda 100644 --- a/libs/service-portal/assets/src/screens/VehicleHistory/HistoryTableData.tsx +++ b/libs/service-portal/assets/src/screens/VehicleHistory/HistoryTableData.tsx @@ -3,7 +3,7 @@ import React, { FC } from 'react' import { Locale } from '@island.is/shared/types' import { Text, Table as T } from '@island.is/island-ui/core' import { VehiclesVehicle } from '@island.is/api/schema' -import { getDateLocale } from '../../utils/constants' +import { getDateLocale } from '@island.is/service-portal/core' interface Props { vehicle: VehiclesVehicle diff --git a/libs/service-portal/assets/src/utils/constants.ts b/libs/service-portal/assets/src/utils/constants.ts index fdb366ac8072..e94beeb244d1 100644 --- a/libs/service-portal/assets/src/utils/constants.ts +++ b/libs/service-portal/assets/src/utils/constants.ts @@ -1,8 +1,4 @@ -import { Locale } from '@island.is/shared/types' - export const VEHICLE_OWNER = 'eigandi' export const VEHICLE_COOWNER = 'meðeigandi' export const VEHICLE_OPERATOR = 'umráðamaður' export const LOCALE = 'is-IS' -export const getDateLocale = (locale: Locale) => - locale === 'en' ? 'en-US' : 'is-IS' diff --git a/libs/service-portal/core/src/components/ConfirmationModal/ConfirmationModal.css.ts b/libs/service-portal/core/src/components/ConfirmationModal/ConfirmationModal.css.ts new file mode 100644 index 000000000000..e0957724492c --- /dev/null +++ b/libs/service-portal/core/src/components/ConfirmationModal/ConfirmationModal.css.ts @@ -0,0 +1,5 @@ +import { style } from '@vanilla-extract/css' + +export const image = style({ + height: '60%', +}) diff --git a/libs/service-portal/core/src/components/ConfirmationModal/ConfirmationModal.tsx b/libs/service-portal/core/src/components/ConfirmationModal/ConfirmationModal.tsx new file mode 100644 index 000000000000..ada8abcc210a --- /dev/null +++ b/libs/service-portal/core/src/components/ConfirmationModal/ConfirmationModal.tsx @@ -0,0 +1,98 @@ +import { + Box, + Button, + GridColumn, + GridRow, + Text, +} from '@island.is/island-ui/core' +import { useLocale, useNamespaces } from '@island.is/localization' +import { FC } from 'react' +import * as styles from './ConfirmationModal.css' +import Modal from '../Modal/Modal' +import LinkResolver from '../LinkResolver/LinkResolver' +import { m } from '../..' +interface Props { + onSubmit: () => void + onCancel: () => void + onClose: () => void + loading: boolean + modalTitle: string + modalText: string + redirectPath: string +} + +export const ConfirmationModal: FC> = ({ + onSubmit, + onCancel, + onClose, + loading, + modalTitle, + modalText, + redirectPath, +}) => { + useNamespaces('service.portal') + const { formatMessage } = useLocale() + + return ( + + + + + + {modalTitle} + {modalText} + + + + + + + + + + + + + + + + + + + + + ) +} diff --git a/libs/service-portal/core/src/components/GeneralButton/GeneralButton.tsx b/libs/service-portal/core/src/components/GeneralButton/GeneralButton.tsx index a497618d7b67..058d171e6414 100644 --- a/libs/service-portal/core/src/components/GeneralButton/GeneralButton.tsx +++ b/libs/service-portal/core/src/components/GeneralButton/GeneralButton.tsx @@ -10,6 +10,7 @@ type Props = Pick< | 'icon' | 'iconType' | 'as' + | 'disabled' > export const GeneralButton = ({ diff --git a/libs/service-portal/core/src/components/InfoLine/InfoLine.tsx b/libs/service-portal/core/src/components/InfoLine/InfoLine.tsx index 6d4e7abbabff..e91d195fe409 100644 --- a/libs/service-portal/core/src/components/InfoLine/InfoLine.tsx +++ b/libs/service-portal/core/src/components/InfoLine/InfoLine.tsx @@ -1,4 +1,3 @@ -import React, { FC } from 'react' import { Box, Text, @@ -10,13 +9,10 @@ import { SkeletonLoader, ButtonProps as CoreButtonProps, Divider, - Button, } from '@island.is/island-ui/core' import { useLocale } from '@island.is/localization' import { MessageDescriptor } from 'react-intl' -import { useLocation } from 'react-router-dom' import { sharedMessages } from '@island.is/shared/translations' - import * as styles from './InfoLine.css' import cn from 'classnames' import { LinkButton } from '../LinkButton/LinkButton' @@ -29,6 +25,7 @@ type ButtonProps = label?: MessageDescriptor | string skipOutboundTrack?: boolean to: string + disabled?: boolean } | { type: 'action' @@ -36,6 +33,8 @@ type ButtonProps = label?: MessageDescriptor | string variant?: 'text' | 'utility' action: () => void + disabled?: boolean + tooltip?: string } interface Props { @@ -63,12 +62,12 @@ export const InfoLine = ({ label, content, renderContent, - labelColumnSpan = ['12/12', '4/12', '6/12', '6/12', '4/12'], - valueColumnSpan = ['1/1', '5/12', '6/12', '6/12', '4/12'], - buttonColumnSpan = ['1/1', '3/12', '3/12', '3/12', '3/12'], loading, button, tooltip, + labelColumnSpan = ['1/1', '5/12', '5/12'], + valueColumnSpan = ['1/1', '5/12', '5/12'], + buttonColumnSpan = ['1/1', '2/12'], paddingY = 2, paddingBottom, warning, @@ -87,7 +86,6 @@ export const InfoLine = ({ position="relative" paddingY={paddingY} paddingBottom={paddingBottom} - paddingRight={4} className={cn(className, { [styles.printable]: printable, })} @@ -144,16 +142,10 @@ export const InfoLine = ({ - {button ? ( + {button && ( ) : ( - - {button.label && formatMessage(button.label)} - + <> + + {button.label && formatMessage(button.label)} + + {button.tooltip && ( + + )} + )} - ) : null} + )} diff --git a/libs/service-portal/core/src/components/IntroHeader/IntroHeader.tsx b/libs/service-portal/core/src/components/IntroHeader/IntroHeader.tsx index 69723e091e6d..b0ec8308b500 100644 --- a/libs/service-portal/core/src/components/IntroHeader/IntroHeader.tsx +++ b/libs/service-portal/core/src/components/IntroHeader/IntroHeader.tsx @@ -40,7 +40,11 @@ export const IntroHeader = (props: IntroHeaderProps & Props) => { const columnSpan = isMobile ? '8/8' : props.narrow ? '4/8' : '5/8' if (props.loading) { - return + return ( + + + + ) } return ( diff --git a/libs/service-portal/core/src/components/LinkButton/LinkButton.tsx b/libs/service-portal/core/src/components/LinkButton/LinkButton.tsx index 229ecf58217a..316e82b595c9 100644 --- a/libs/service-portal/core/src/components/LinkButton/LinkButton.tsx +++ b/libs/service-portal/core/src/components/LinkButton/LinkButton.tsx @@ -50,8 +50,8 @@ export const LinkButton = ({ size={size ?? 'small'} variant="text" unfocusable - icon={isExternal ? 'open' : undefined} - iconType={isExternal ? 'outline' : undefined} + icon={isExternal ? 'open' : icon} + iconType="outline" > {text} diff --git a/libs/service-portal/core/src/components/UserInfoLine/UserInfoLine.tsx b/libs/service-portal/core/src/components/UserInfoLine/UserInfoLine.tsx index 14929bcee17b..daaae7799982 100644 --- a/libs/service-portal/core/src/components/UserInfoLine/UserInfoLine.tsx +++ b/libs/service-portal/core/src/components/UserInfoLine/UserInfoLine.tsx @@ -9,10 +9,10 @@ import { ResponsiveSpace, SkeletonLoader, ButtonProps, + Button, } from '@island.is/island-ui/core' import { useLocale } from '@island.is/localization' import { MessageDescriptor } from 'react-intl' -import { useLocation } from 'react-router-dom' import { sharedMessages } from '@island.is/shared/translations' import * as styles from './UserInfoLine.css' @@ -27,6 +27,11 @@ export type EditLink = { icon?: ButtonProps['icon'] } +type LineButton = { + title: MessageDescriptor | string + onClick: () => void +} + interface Props { label: MessageDescriptor | string content?: string | JSX.Element @@ -37,6 +42,7 @@ interface Props { valueColumnSpan?: GridColumnProps['span'] editColumnSpan?: GridColumnProps['span'] editLink?: EditLink + button?: LineButton title?: string titlePadding?: ResponsiveSpace tooltip?: string @@ -69,8 +75,8 @@ export const UserInfoLine: FC> = ({ translateLabel = 'yes', printable = false, tooltipFull, + button, }) => { - const { pathname } = useLocation() const { formatMessage } = useLocale() return ( @@ -103,7 +109,7 @@ export const UserInfoLine: FC> = ({ as="span" lineHeight="lg" > - {formatMessage(label)}{' '} + {formatMessage(label)} {tooltip && ( > = ({ skipOutboundTrack={editLink.skipOutboundTrack} /> - ) : null} + ) : button ? ( + + + + ) : undefined} diff --git a/libs/service-portal/core/src/index.ts b/libs/service-portal/core/src/index.ts index 25c5a74f64a0..ebb39ab40443 100644 --- a/libs/service-portal/core/src/index.ts +++ b/libs/service-portal/core/src/index.ts @@ -10,6 +10,7 @@ export * from './components/EmptyState/EmptyImage' export * from './components/EmptyState/EmptyImgSmall' export * from './components/Menu/Menu' export * from './components/Modal/Modal' +export * from './components/ConfirmationModal/ConfirmationModal' export * from './components/UserInfoLine/UserInfoLine' export * from './components/InfoLine/InfoLine' export * from './components/InfoLine/InfoLineStack' diff --git a/libs/service-portal/core/src/lib/messages.ts b/libs/service-portal/core/src/lib/messages.ts index 22d1c72c4b53..fc72b9ef77cc 100644 --- a/libs/service-portal/core/src/lib/messages.ts +++ b/libs/service-portal/core/src/lib/messages.ts @@ -1386,6 +1386,11 @@ export const m = defineMessages({ id: 'service.portal:mms-tooltip-secondary', defaultMessage: 'Menntamálastofnun hefur umsjón með gögnum um menntaskóla.', }, + domsmalaraduneytidTooltip: { + id: 'service.portal:dmr-tooltip', + defaultMessage: + 'Dómsmálaráðuneytið hefur umsjón með gögnum um dómsmál og þeim tengt.', + }, occupationalLicenseTooltip: { id: 'service.portal:occupational-license-tooltip', defaultMessage: @@ -1629,10 +1634,52 @@ export const m = defineMessages({ defaultMessage: 'Ef þú telur að þú eigir að vera með skráðann {arg}, vinsamlegast hafðu samband við þjónustuaðila.', }, + lawAndOrder: { + id: 'service.portal:law-and-order', + defaultMessage: 'Lög og reglur', + }, + lawAndOrderDashboard: { + id: 'service.portal:law-and-order-dashboard', + defaultMessage: 'Þín mál í dómskerfinu', + }, + lawAndOrderDescription: { + id: 'service.portal:law-and-order-description', + defaultMessage: + 'Hér eru upplýsingar og yfirlit yfir mál sem þú átt hjá dómskerfinu.', + }, + courtCases: { + id: 'service.portal:court-cases', + defaultMessage: 'Dómsmál', + }, + subpoena: { + id: 'service.portal:subpoena', + defaultMessage: 'Fyrirkall', + }, + urgent: { + id: 'service.portal:urgent', + defaultMessage: 'Áríðandi', + }, + openErrand: { + id: 'service.portal:open-errand', + defaultMessage: 'Opna erindi', + }, readMoreAbout: { id: 'service.portal:read-more-about', defaultMessage: 'Lesa meira um {arg}', }, + acknowledgeTitle: { + id: 'sp.service.portal:acknowledge-title', + defaultMessage: 'Staðfesting á móttöku', + }, + acknowledgeText: { + id: 'sp.service.portal:acknowledge-text', + defaultMessage: + 'Þú ert að opna erindi frá {arg}. Veljir þú að opna erindið fá viðeigandi aðilar senda staðfestingu á möttöku. Veljir þú að opna ekki erindið munu viðeigandi aðilar leita annarra leiða til að afhenda þér það, t.d. með aðstoð lögreglu.', + }, + acknowledgementCompleted: { + id: 'sp.service.portal:acknowledgement-completed', + defaultMessage: 'Staðfesting á móttöku tókst', + }, submit: { id: 'service.portal:submit', defaultMessage: 'Staðfesta', diff --git a/libs/service-portal/core/src/utils/constants.ts b/libs/service-portal/core/src/utils/constants.ts index 5c3823234aa8..0f5ca7e0bf73 100644 --- a/libs/service-portal/core/src/utils/constants.ts +++ b/libs/service-portal/core/src/utils/constants.ts @@ -17,6 +17,7 @@ export const UNI_HI_SLUG = 'haskoli-islands' export const ISLAND_SYSLUMENN_SLUG = '/s/islandis' export const HEALTH_DIRECTORATE_SLUG = 'landlaeknir' export const HUGVERKASTOFAN_SLUG = 'hugverkastofan' +export const DOMSMALARADUNEYTID_SLUG = 'domsmalaraduneytid' export const MONTHS = [ 'january', diff --git a/libs/service-portal/core/src/utils/dateUtils.ts b/libs/service-portal/core/src/utils/dateUtils.ts index 7bf884365b02..b3b5f1122289 100644 --- a/libs/service-portal/core/src/utils/dateUtils.ts +++ b/libs/service-portal/core/src/utils/dateUtils.ts @@ -81,3 +81,6 @@ export const displayMonthOrYear = (date: string, l: Locale) => { return date } } + +export const getDateLocale = (locale: Locale) => + locale === 'en' ? 'en-US' : 'is-IS' diff --git a/libs/service-portal/documents/src/components/DocumentActions/DocumentActionsV3.tsx b/libs/service-portal/documents/src/components/DocumentActions/DocumentActionsV3.tsx new file mode 100644 index 000000000000..fbe5092c62a3 --- /dev/null +++ b/libs/service-portal/documents/src/components/DocumentActions/DocumentActionsV3.tsx @@ -0,0 +1,72 @@ +import { AlertMessage, Box, Button } from '@island.is/island-ui/core' +import { IconMapIcon } from '@island.is/island-ui/core/types' +import { sendForm } from '../../utils/downloadDocumentV2' +import { useUserInfo } from '@island.is/auth/react' +import { useDocumentContext } from '../../screens/Overview/DocumentContext' + +const DocumentActions = () => { + const { activeDocument } = useDocumentContext() + const userInfo = useUserInfo() + const DEFAULT_ICON: IconMapIcon = 'document' + const actions = activeDocument?.actions + const alert = activeDocument?.alert + + return ( + + {alert && ( + + + + )} + {actions && ( + + {actions.map((a) => { + return ( + + {a.type === 'url' && a.data && ( + + + + )} + {a.type === 'file' && activeDocument && ( + + )} + + ) + })} + + )} + + ) +} + +export default DocumentActions diff --git a/libs/service-portal/documents/src/components/DocumentHeader/DocumentHeaderV3.tsx b/libs/service-portal/documents/src/components/DocumentHeader/DocumentHeaderV3.tsx new file mode 100644 index 000000000000..533291e960c7 --- /dev/null +++ b/libs/service-portal/documents/src/components/DocumentHeader/DocumentHeaderV3.tsx @@ -0,0 +1,86 @@ +import { useEffect, useRef } from 'react' +import { DocumentV2Action, DocumentsV2Category } from '@island.is/api/schema' +import { Box, Text } from '@island.is/island-ui/core' +import { helperStyles } from '@island.is/island-ui/theme' +import AvatarImage from '../DocumentLine/AvatarImage' +import { + DocumentActionBar, + DocumentActionBarProps, +} from '../DocumentActionBar/DocumentActionBarV2' +import DocumentActions from '../DocumentActions/DocumentActionsV3' +import * as styles from './DocumentHeader.css' + +type DocumentHeaderProps = { + avatar?: string + sender?: string + date?: string + category?: DocumentsV2Category + actionBar?: DocumentActionBarProps + actions?: DocumentV2Action[] + subject?: string +} + +export const DocumentHeader: React.FC = ({ + avatar, + sender, + date, + category, + actionBar, + actions, + subject, +}) => { + const wrapper = useRef(null) + + useEffect(() => { + if (wrapper.current) { + wrapper.current.focus() + } + }, [wrapper]) + + return ( + <> + +

+ {subject} +

+ {avatar && } + + {sender && ( + + {sender} + + )} + + {date && {date}} + {category && ( + + {category.name ?? ''} + + )} + + + {actionBar && ( + + + + )} +
+ {actions && ( + + + + )} + + ) +} diff --git a/libs/service-portal/documents/src/components/DocumentLine/DocumentLine.css.ts b/libs/service-portal/documents/src/components/DocumentLine/DocumentLine.css.ts index e94de7c80748..8f02f3b57851 100644 --- a/libs/service-portal/documents/src/components/DocumentLine/DocumentLine.css.ts +++ b/libs/service-portal/documents/src/components/DocumentLine/DocumentLine.css.ts @@ -71,3 +71,30 @@ export const checkCircle = style({ maxWidth: 30, transition: 'background-color .25s', }) + +export const linkWrapper = style({ + backgroundColor: 'unset', + ...themeUtils.responsiveStyle({ + sm: { + backgroundColor: theme.color.blueberry100, + }, + }), +}) + +export const badge = style({ + position: 'absolute', + top: -12, + bottom: 0, + right: 'auto', + left: -107, + height: theme.spacing[1], + width: theme.spacing[1], + borderRadius: '50%', + backgroundColor: theme.color.red400, +}) + +export const avatar = style({ + display: 'flex', + justifyContent: 'center', + alignItems: 'center', +}) diff --git a/libs/service-portal/documents/src/components/DocumentLine/DocumentLineV3.tsx b/libs/service-portal/documents/src/components/DocumentLine/DocumentLineV3.tsx new file mode 100644 index 000000000000..499477b92e30 --- /dev/null +++ b/libs/service-portal/documents/src/components/DocumentLine/DocumentLineV3.tsx @@ -0,0 +1,383 @@ +import cn from 'classnames' +import format from 'date-fns/format' +import { FC, useEffect, useRef, useState } from 'react' + +import { + DocumentV2, + DocumentV2Action, + DocumentV2Content, +} from '@island.is/api/schema' +import { Box, Icon, LoadingDots, Text, toast } from '@island.is/island-ui/core' +import { useLocale } from '@island.is/localization' +import { ConfirmationModal, m } from '@island.is/service-portal/core' +import { dateFormat } from '@island.is/shared/constants' +import { + matchPath, + useLocation, + useNavigate, + useParams, +} from 'react-router-dom' +import { useDocumentList } from '../../hooks/useDocumentListV3' +import { useIsChildFocusedorHovered } from '../../hooks/useIsChildFocused' +import { useMailAction } from '../../hooks/useMailActionV2' +import { DocumentsPaths } from '../../lib/paths' +import { useDocumentContext } from '../../screens/Overview/DocumentContext' +import { useGetDocumentInboxLineV3LazyQuery } from '../../screens/Overview/Overview.generated' +import { messages } from '../../utils/messages' +import { FavAndStashV3 } from '../FavAndStash/FavAndStashV3' +import UrgentTag from '../UrgentTag/UrgentTag' +import AvatarImage from './AvatarImage' +import * as styles from './DocumentLine.css' + +interface Props { + documentLine: DocumentV2 + img?: string + setSelectLine?: (id: string) => void + active: boolean + asFrame?: boolean + includeTopBorder?: boolean + selected?: boolean + bookmarked?: boolean +} + +export const DocumentLineV3: FC = ({ + documentLine, + img, + setSelectLine, + active, + asFrame, + includeTopBorder, + bookmarked, + selected, +}) => { + const [hasFocusOrHover, setHasFocusOrHover] = useState(false) + const [hasAvatarFocus, setHasAvatarFocus] = useState(false) + const [modalData, setModalData] = useState<{ + title: string + text: string + } | null>(null) + const [isModalVisible, setModalVisible] = useState(false) + + const { formatMessage, lang } = useLocale() + const navigate = useNavigate() + const location = useLocation() + const date = format(new Date(documentLine.publicationDate), dateFormat.is) + const { id } = useParams<{ + id: string + }>() + const isUrgent = documentLine.isUrgent + const { + submitMailAction, + loading: postLoading, + bookmarkSuccess, + } = useMailAction() + + const { fetchObject, refetch } = useDocumentList() + + const { + setActiveDocument, + setDocumentDisplayError, + setDocLoading, + setLocalRead, + localRead, + } = useDocumentContext() + + const toggleModal = () => { + setModalVisible(!isModalVisible) + } + + const wrapperRef = useRef(null) + const avatarRef = useRef(null) + + const isFocused = useIsChildFocusedorHovered(wrapperRef) + const isAvatarFocused = useIsChildFocusedorHovered(avatarRef, false) + + useEffect(() => { + setHasFocusOrHover(isFocused) + }, [isFocused]) + + useEffect(() => { + setHasAvatarFocus(isAvatarFocused) + }, [isAvatarFocused]) + + const displayPdf = ( + content?: DocumentV2Content, + actions?: Array, + ) => { + setActiveDocument({ + document: { + type: content?.type, + value: content?.value, + }, + id: documentLine.id, + sender: documentLine.sender?.name ?? '', + subject: documentLine.subject, + senderNatReg: documentLine.sender?.id ?? '', + downloadUrl: documentLine.downloadUrl ?? '', + date: date, + img, + categoryId: documentLine.categoryId ?? undefined, + actions: actions, + alert: documentLine.alert ?? undefined, + }) + window.scrollTo({ + top: 0, + behavior: 'smooth', + }) + } + + const [getDocument, { loading: fileLoading }] = + useGetDocumentInboxLineV3LazyQuery({ + variables: { + input: { + id: documentLine.id, + provider: documentLine.sender?.name ?? 'unknown', + }, + locale: lang, + }, + fetchPolicy: 'no-cache', + onCompleted: (data) => { + if (asFrame) { + navigate( + DocumentsPaths.ElectronicDocumentSingle.replace( + ':id', + documentLine.id, + ), + ) + } else { + const docContent = data?.documentV2?.content + const actions = data?.documentV2?.actions ?? undefined + if (docContent) { + displayPdf(docContent, actions) + setDocumentDisplayError(undefined) + setLocalRead([...localRead, documentLine.id]) + } else { + setDocumentDisplayError(formatMessage(messages.documentErrorLoad)) + } + } + }, + onError: () => { + const errorMessage = formatMessage(messages.documentFetchError, { + senderName: documentLine.sender?.name ?? '', + }) + if (asFrame) { + toast.error(errorMessage, { toastId: 'overview-doc-error' }) + } else { + setDocumentDisplayError(errorMessage) + } + }, + }) + + const [getDocumentMetadata, { loading: metadataLoading }] = + useGetDocumentInboxLineV3LazyQuery({ + variables: { + input: { + id: documentLine.id, + provider: documentLine.sender?.name ?? 'unknown', + includeDocument: false, + }, + }, + fetchPolicy: 'no-cache', + onCompleted: (data) => { + const actions: DocumentV2Action | undefined | null = + data?.documentV2?.confirmation + + setDocumentDisplayError(undefined) + if (actions) { + setModalData({ + title: actions.title ?? '', + text: actions.data ?? '', + }) + setModalVisible(true) + } else { + getDocument() + } + }, + onError: () => { + setDocumentDisplayError( + formatMessage(messages.documentFetchError, { + senderName: documentLine.sender?.name ?? '', + }), + ) + }, + }) + + useEffect(() => { + if (id === documentLine.id) { + // If the document is marked as urgent, the user needs to acknowledge the document before opening it. + if (isUrgent && !asFrame) { + getDocumentMetadata() + } else { + getDocument() + } + } + }, [id, documentLine, getDocument]) + + useEffect(() => { + setDocLoading(fileLoading) + }, [fileLoading, setDocLoading]) + + // If document is marked as urgent, the user needs to acknowledge the document before opening it + // This is done by calling "getDocument" with "includeDocument=false" and receive the metadata about the document + // This metadata includes actions array that should include an action with type 'confirmation' (also title and data), + // which will be used to display a confirmation modal to the user + const onLineClick = async () => { + const pathName = location.pathname + const match = matchPath( + { + path: DocumentsPaths.ElectronicDocumentSingle, + }, + pathName, + ) + if (match?.params?.id && match?.params?.id !== documentLine?.id) { + navigate(DocumentsPaths.ElectronicDocumentsRoot, { replace: true }) + } + if (isUrgent && !asFrame) { + getDocumentMetadata() + } else { + getDocument() + } + } + + const unread = !documentLine.opened && !localRead.includes(documentLine.id) + const isBookmarked = bookmarked || bookmarkSuccess + + return ( + + +
+ { + e.stopPropagation() + if (documentLine.id && setSelectLine && !isUrgent) { + setSelectLine(documentLine.id) + } + }} + avatar={ + (hasAvatarFocus || selected) && !asFrame && !isUrgent ? ( + + + + ) : undefined + } + background={ + hasAvatarFocus && !isUrgent + ? asFrame + ? 'white' + : 'blue200' + : documentLine.opened + ? 'blue100' + : 'white' + } + /> +
+ + {active &&
} + + + {documentLine.sender?.name ?? ''} + + {date} + + + + + + {(postLoading || fileLoading || metadataLoading) && ( + + + + )} + {(hasFocusOrHover || isBookmarked) && + !postLoading && + !fileLoading && + !asFrame && ( + { + e.stopPropagation() + await submitMailAction( + isBookmarked ? 'unbookmark' : 'bookmark', + documentLine.id, + ) + refetch(fetchObject) + } + : undefined + } + /> + )} + {isUrgent && } + + + + + {isModalVisible && ( + { + setModalVisible(false) + getDocument() + }} + onCancel={() => setModalVisible(false)} + onClose={toggleModal} + loading={false} + modalTitle={modalData?.title || formatMessage(m.acknowledgeTitle)} + modalText={ + modalData?.text || + formatMessage(m.acknowledgeText, { + arg: documentLine.sender.name, + }) + } + redirectPath={''} + /> + )} + + ) +} + +export default DocumentLineV3 diff --git a/libs/service-portal/documents/src/components/FavAndStash/FavAndStashV3.tsx b/libs/service-portal/documents/src/components/FavAndStash/FavAndStashV3.tsx new file mode 100644 index 000000000000..04fda5f7b742 --- /dev/null +++ b/libs/service-portal/documents/src/components/FavAndStash/FavAndStashV3.tsx @@ -0,0 +1,85 @@ +import { Box, Button, LoadingDots } from '@island.is/island-ui/core' +import { useLocale } from '@island.is/localization' +import { Tooltip, m } from '@island.is/service-portal/core' +import React, { MouseEvent } from 'react' +import * as styles from './FavAndStash.css' + +type FavAndStashProps = { + onFav?: (event: MouseEvent) => void + onStash?: (event: MouseEvent) => void + onRead?: (event: MouseEvent) => void + bookmarked?: boolean + archived?: boolean + loading?: boolean +} + +export const FavAndStashV3: React.FC = ({ + onFav, + onStash, + onRead, + bookmarked, + archived, + loading, +}) => { + const { formatMessage } = useLocale() + + if (loading) { + return ( + + + + ) + } + + return ( + + {onStash && ( + + + + )} + + ) +} + +export default DesktopOverview diff --git a/libs/service-portal/documents/src/components/OverviewDisplay/MobileOverviewV2.tsx b/libs/service-portal/documents/src/components/OverviewDisplay/MobileOverviewV2.tsx index 5bcaaa84de60..db88c10f3f4a 100644 --- a/libs/service-portal/documents/src/components/OverviewDisplay/MobileOverviewV2.tsx +++ b/libs/service-portal/documents/src/components/OverviewDisplay/MobileOverviewV2.tsx @@ -5,7 +5,7 @@ import { Box, Text, GridColumn, GridRow } from '@island.is/island-ui/core' import { DocumentsV2Category } from '@island.is/api/schema' import { useLocale, useNamespaces } from '@island.is/localization' import { DocumentRenderer } from '../DocumentRenderer/DocumentRendererV2' -import { DocumentHeader } from '../../components/DocumentHeader/DocumentHeaderV2' +import { DocumentHeader } from '../DocumentHeader/DocumentHeaderV2' import { DocumentActionBar } from '../../components/DocumentActionBar/DocumentActionBarV2' import { useDocumentContext } from '../../screens/Overview/DocumentContext' import * as styles from './OverviewDisplay.css' diff --git a/libs/service-portal/documents/src/components/OverviewDisplay/MobileOverviewV3.tsx b/libs/service-portal/documents/src/components/OverviewDisplay/MobileOverviewV3.tsx new file mode 100644 index 000000000000..c98d4a65c5d0 --- /dev/null +++ b/libs/service-portal/documents/src/components/OverviewDisplay/MobileOverviewV3.tsx @@ -0,0 +1,72 @@ +import { FC } from 'react' +import FocusLock from 'react-focus-lock' +import { LoadModal, m } from '@island.is/service-portal/core' +import { Box, Text, GridColumn, GridRow } from '@island.is/island-ui/core' +import { DocumentsV2Category } from '@island.is/api/schema' +import { useLocale, useNamespaces } from '@island.is/localization' +import { DocumentRenderer } from '../DocumentRenderer/DocumentRendererV2' +import { DocumentHeader } from '../DocumentHeader/DocumentHeaderV3' +import { DocumentActionBar } from '../../components/DocumentActionBar/DocumentActionBarV2' +import { useDocumentContext } from '../../screens/Overview/DocumentContext' +import * as styles from './OverviewDisplay.css' + +interface Props { + onPressBack: () => void + activeBookmark: boolean + loading?: boolean + category?: DocumentsV2Category +} + +export const MobileOverview: FC = ({ + onPressBack, + activeBookmark, + category, + loading, +}) => { + useNamespaces('sp.documents') + const { formatMessage } = useLocale() + const { activeDocument } = useDocumentContext() + + if (loading) { + return + } + + if (!activeDocument) { + return null + } + + return ( + + + + + + + + + + + {activeDocument?.subject} + + {} + + + + + + ) +} + +export default MobileOverview diff --git a/libs/service-portal/documents/src/components/OverviewDisplay/OverviewDocumentDisplayV3.tsx b/libs/service-portal/documents/src/components/OverviewDisplay/OverviewDocumentDisplayV3.tsx new file mode 100644 index 000000000000..8b64bb600373 --- /dev/null +++ b/libs/service-portal/documents/src/components/OverviewDisplay/OverviewDocumentDisplayV3.tsx @@ -0,0 +1,44 @@ +import { FC } from 'react' +import { toast } from '@island.is/island-ui/core' +import { theme } from '@island.is/island-ui/theme' +import { DocumentsV2Category } from '@island.is/api/schema' +import useWindowSize from 'react-use/lib/useWindowSize' +import DesktopOverview from './DesktopOverviewV3' +import MobileOverview from './MobileOverviewV3' +import NoPDF from '../NoPDF/NoPDF' + +export interface Props { + onPressBack: () => void + activeBookmark: boolean + loading?: boolean + error?: { + message?: string + code: 'list' | 'single' + } + category?: DocumentsV2Category +} + +export const DocumentDisplay: FC = (props) => { + const { width } = useWindowSize() + + const isDesktop = width > theme.breakpoints.lg + + if (props.error?.message) { + if (!isDesktop) { + toast.error(props.error?.message, { toastId: 'single-doc-error' }) + } + if (props.error?.code === 'single' && !isDesktop) { + return null + } + return + } + + if (isDesktop) { + const { onPressBack, ...deskProps } = props + return + } + + return +} + +export default DocumentDisplay diff --git a/libs/service-portal/documents/src/components/UrgentTag/UrgentTag.tsx b/libs/service-portal/documents/src/components/UrgentTag/UrgentTag.tsx new file mode 100644 index 000000000000..c7fdf9f8854f --- /dev/null +++ b/libs/service-portal/documents/src/components/UrgentTag/UrgentTag.tsx @@ -0,0 +1,31 @@ +import { Box, Icon, Text } from '@island.is/island-ui/core' +import { useLocale } from '@island.is/localization' +import { m } from '@island.is/service-portal/core' + +const UrgentTag = () => { + const { formatMessage } = useLocale() + return ( + + + + + {formatMessage(m.urgent)} + + + + ) +} + +export default UrgentTag diff --git a/libs/service-portal/documents/src/hooks/useDocumentListV3.ts b/libs/service-portal/documents/src/hooks/useDocumentListV3.ts new file mode 100644 index 000000000000..6baee903493a --- /dev/null +++ b/libs/service-portal/documents/src/hooks/useDocumentListV3.ts @@ -0,0 +1,106 @@ +import { useEffect } from 'react' +import { AuthDelegationType } from '@island.is/api/schema' +import { useUserInfo } from '@island.is/auth/react' +import { useDocumentContext } from '../screens/Overview/DocumentContext' +import { useDocumentsV3Query } from '../screens/Overview/Overview.generated' +import differenceInYears from 'date-fns/differenceInYears' + +export const pageSize = 10 + +type UseDocumentListProps = { defaultPageSize?: number } + +export const useDocumentList = (props?: UseDocumentListProps) => { + const { + filterValue, + page, + totalPages, + categoriesAvailable, + sendersAvailable, + + setTotalPages, + setCategoriesAvailable, + setSendersAvailable, + } = useDocumentContext() + + const userInfo = useUserInfo() + const isLegal = userInfo.profile.delegationType?.includes( + AuthDelegationType.LegalGuardian, + ) + const dateOfBirth = userInfo?.profile.dateOfBirth + let isOver15 = false + if (dateOfBirth) { + isOver15 = differenceInYears(new Date(), dateOfBirth) > 15 + } + const hideHealthData = isOver15 && isLegal + + const fetchObject = { + input: { + senderNationalId: filterValue.activeSenders, + dateFrom: filterValue.dateFrom?.toISOString(), + dateTo: filterValue.dateTo?.toISOString(), + categoryIds: filterValue.activeCategories, + subjectContains: filterValue.searchQuery, + typeId: null, + opened: filterValue.showUnread ? false : null, + page: page, + pageSize: props?.defaultPageSize ?? pageSize, + isLegalGuardian: hideHealthData, + archived: filterValue.archived, + bookmarked: filterValue.bookmarked, + }, + } + + const { data, loading, error, client, refetch } = useDocumentsV3Query({ + variables: fetchObject, + }) + + const invalidateCache = async () => { + client.cache.evict({ + id: 'ROOT_QUERY', + fieldName: 'documentsV2', + }) + client.cache.gc() + } + + useEffect(() => { + if ( + !loading && + data?.documentsV2?.senders && + sendersAvailable.length === 0 + ) { + setSendersAvailable(data.documentsV2.senders) + } + + if ( + !loading && + data?.documentsV2?.categories && + categoriesAvailable.length === 0 + ) { + setCategoriesAvailable(data.documentsV2.categories) + } + }, [loading, data, sendersAvailable, categoriesAvailable]) + + const totalCount = data?.documentsV2?.totalCount || 0 + useEffect(() => { + const pageCount = Math.ceil(totalCount / pageSize) + if (pageCount !== totalPages && !loading) { + setTotalPages(pageCount) + } + }, [pageSize, totalCount, loading]) + + const filteredDocuments = data?.documentsV2?.data || [] + const activeArchive = filterValue.archived === true + return { + activeArchive, + filteredDocuments, + totalCount, + totalPages, + fetchObject, + + data, + loading, + error, + refetch, + invalidateCache, + } +} diff --git a/libs/service-portal/documents/src/hooks/useMailActionV2.ts b/libs/service-portal/documents/src/hooks/useMailActionV2.ts index 43ce1a573169..40b09d945808 100644 --- a/libs/service-portal/documents/src/hooks/useMailActionV2.ts +++ b/libs/service-portal/documents/src/hooks/useMailActionV2.ts @@ -1,4 +1,3 @@ -import { gql, useMutation } from '@apollo/client' import { useState } from 'react' import { toast } from '@island.is/island-ui/core' import { useLocale } from '@island.is/localization' diff --git a/libs/service-portal/documents/src/index.ts b/libs/service-portal/documents/src/index.ts index ad55b8044893..25d2f1e4028d 100644 --- a/libs/service-portal/documents/src/index.ts +++ b/libs/service-portal/documents/src/index.ts @@ -1,6 +1,8 @@ export * from './module' export * from './lib/navigation' export * from './lib/paths' +export * from './components/DocumentLine/DocumentLineV3' export * from './components/DocumentLine/DocumentLineV2' + export * from './components/DocumentLine/AvatarImage' -export * from './hooks/useDocumentList' +export * from './hooks/useDocumentListV3' diff --git a/libs/service-portal/documents/src/lib/types.ts b/libs/service-portal/documents/src/lib/types.ts index af8bd48e6aac..0669d4a93be9 100644 --- a/libs/service-portal/documents/src/lib/types.ts +++ b/libs/service-portal/documents/src/lib/types.ts @@ -1,4 +1,8 @@ -import { DocumentDetails, DocumentV2Content } from '@island.is/api/schema' +import { + DocumentDetails, + DocumentV2Content, + DocumentV2Action, +} from '@island.is/api/schema' type ActiveDoc = { id: string @@ -9,6 +13,8 @@ type ActiveDoc = { img?: string categoryId?: string senderNatReg?: string + actions?: Array + alert?: DocumentV2Action } export type ActiveDocumentType = { diff --git a/libs/service-portal/documents/src/screens/Overview/Overview.graphql b/libs/service-portal/documents/src/screens/Overview/Overview.graphql index 4c4dcf61d663..0a481efd364b 100644 --- a/libs/service-portal/documents/src/screens/Overview/Overview.graphql +++ b/libs/service-portal/documents/src/screens/Overview/Overview.graphql @@ -33,6 +33,69 @@ query DocumentsV2($input: DocumentsV2DocumentsInput!) { } } +query DocumentsV3($input: DocumentsV2DocumentsInput!) { + documentsV2(input: $input) { + data { + id + name + categoryId + publicationDate + documentDate + subject + sender { + id + name + } + opened + bookmarked + archived + downloadUrl + content { + type + value + } + isUrgent + confirmation { + title + data + } + } + totalCount + unreadCount + categories { + id + name + } + senders { + id + name + } + } +} + +query GetDocumentInboxLineV3($input: DocumentInput!, $locale: String) { + documentV2(input: $input, locale: $locale) { + content { + type + value + } + actions { + type + title + data + icon + } + confirmation { + title + data + } + alert { + title + data + } + } +} + query GetDocumentInboxLineV2($input: DocumentInput!) { documentV2(input: $input) { content { diff --git a/libs/service-portal/documents/src/screens/Overview/OverviewV3.tsx b/libs/service-portal/documents/src/screens/Overview/OverviewV3.tsx new file mode 100644 index 000000000000..806ff02558f9 --- /dev/null +++ b/libs/service-portal/documents/src/screens/Overview/OverviewV3.tsx @@ -0,0 +1,303 @@ +import { + Box, + Checkbox, + GridColumn, + GridContainer, + GridRow, + Pagination, + SkeletonLoader, + Stack, + Text, +} from '@island.is/island-ui/core' +import { useLocale, useNamespaces } from '@island.is/localization' +import { GoBack, m, useScrollTopOnUpdate } from '@island.is/service-portal/core' +import { useOrganizations } from '@island.is/service-portal/graphql' +import { getOrganizationLogoUrl } from '@island.is/shared/utils' +import debounce from 'lodash/debounce' +import { useEffect, useMemo } from 'react' +import { useLocation, useNavigate } from 'react-router-dom' +import DocumentsFilter from '../../components/DocumentFilter/DocumentsFilterV2' +import DocumentLine from '../../components/DocumentLine/DocumentLineV3' +import { FavAndStashV3 } from '../../components/FavAndStash/FavAndStashV3' +import DocumentDisplay from '../../components/OverviewDisplay/OverviewDocumentDisplayV3' +import { useDocumentFilters } from '../../hooks/useDocumentFilters' +import { pageSize, useDocumentList } from '../../hooks/useDocumentListV3' +import { useKeyDown } from '../../hooks/useKeyDown' +import { useMailAction } from '../../hooks/useMailActionV2' +import { DocumentsPaths } from '../../lib/paths' +import { messages } from '../../utils/messages' +import { useDocumentContext } from './DocumentContext' +import * as styles from './Overview.css' + +export const ServicePortalDocumentsV3 = () => { + useNamespaces('sp.documents') + const { formatMessage } = useLocale() + const navigate = useNavigate() + const location = useLocation() + const { data: organizations } = useOrganizations() + + const { + selectedLines, + activeDocument, + filterValue, + page, + categoriesAvailable, + sendersAvailable, + docLoading, + documentDisplayError, + + setSelectedLines, + setActiveDocument, + setFilterValue, + } = useDocumentContext() + + const { + loading, + error, + activeArchive, + totalPages, + filteredDocuments, + totalCount, + } = useDocumentList() + + const { handlePageChange, handleSearchChange } = useDocumentFilters() + + const { submitBatchAction, loading: batchActionLoading } = useMailAction() + + useScrollTopOnUpdate([page]) + + useEffect(() => { + if (location?.state?.doc) { + setActiveDocument(location.state.doc) + } + }, [location?.state?.doc]) + + useEffect(() => { + return () => { + debouncedResults.cancel() + } + }) + + useKeyDown('Escape', () => { + setActiveDocument(null) + navigate(DocumentsPaths.ElectronicDocumentsRoot, { + replace: true, + }) + }) + + const debouncedResults = useMemo(() => { + return debounce(handleSearchChange, 500) + }, []) + + const rowDirection = error ? 'column' : 'columnReverse' + + return ( + + + + + + + + + + + + + + setFilterValue((oldFilter) => ({ + ...oldFilter, + activeCategories: [], + })) + } + clearSenders={() => + setFilterValue((oldFilter) => ({ + ...oldFilter, + activeSenders: [], + })) + } + documentsLength={totalCount} + /> + + + + + 0} + onChange={(e) => { + if (e.target.checked) { + const allDocumentIds = filteredDocuments + .filter((x) => !x.isUrgent) + .map((item) => item.id) + setSelectedLines([...allDocumentIds]) + } else { + setSelectedLines([]) + } + }} + /> + + {selectedLines.length > 0 ? null : ( + {formatMessage(m.info)} + )} + + + {selectedLines.length > 0 ? ( + + submitBatchAction( + activeArchive ? 'unarchive' : 'archive', + selectedLines, + filteredDocuments.length === selectedLines.length, + ) + } + archived={activeArchive} + onFav={() => submitBatchAction('bookmark', selectedLines)} + onRead={() => submitBatchAction('read', selectedLines)} + /> + ) : ( + {formatMessage(m.date)} + )} + + {loading && ( + + + + )} + + {filteredDocuments.map((doc) => ( + + { + if (selectedLines.includes(doc.id)) { + const filtered = selectedLines.filter( + (item) => item !== doc.id, + ) + setSelectedLines([...filtered]) + } else { + // Urgent documents can't be selected and marked "read" + // Can be put in storage when being opened otherwise not + if (!doc.isUrgent) { + setSelectedLines([...selectedLines, docId]) + } + } + }} + /> + + ))} + {totalPages ? ( + + ( + + )} + /> + + ) : undefined} + + + + + doc?.id === activeDocument?.id, + )?.[0]?.bookmarked + } + category={categoriesAvailable.find( + (i) => i.id === activeDocument?.categoryId, + )} + onPressBack={() => { + setActiveDocument(null) + navigate(DocumentsPaths.ElectronicDocumentsRoot, { + replace: true, + }) + }} + error={{ + message: error + ? formatMessage(messages.error) + : documentDisplayError ?? undefined, + code: error ? 'list' : 'single', + }} + loading={docLoading} + /> + + + + ) +} + +export default ServicePortalDocumentsV3 diff --git a/libs/service-portal/documents/src/screens/Overview/index.tsx b/libs/service-portal/documents/src/screens/Overview/index.tsx index 4adee80c881c..17a2b220fd91 100644 --- a/libs/service-portal/documents/src/screens/Overview/index.tsx +++ b/libs/service-portal/documents/src/screens/Overview/index.tsx @@ -1,15 +1,45 @@ +import { useFeatureFlagClient } from '@island.is/react/feature-flags' import ServicePortalDocumentsV2 from './OverviewV2' +import ServicePortalDocumentsV3 from './OverviewV3' +import { useEffect, useState } from 'react' import { DocumentsProvider } from './DocumentContext' import { Box } from '@island.is/island-ui/core' export const DocumentIndex = () => { - return ( - - - - - - ) + const featureFlagClient = useFeatureFlagClient() + const [v3Enabled, setV3Enabled] = useState() + + useEffect(() => { + const isFlagEnabled = async () => { + const ffEnabled = await featureFlagClient.getValue( + `isServicePortalDocumentsV3PageEnabled`, + false, + ) + setV3Enabled(ffEnabled as boolean) + } + isFlagEnabled() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + if (v3Enabled) { + return ( + + + + + + ) + } + if (v3Enabled === false) { + return ( + + + + + + ) + } + return null } export default DocumentIndex diff --git a/libs/service-portal/documents/src/utils/downloadDocumentV2.ts b/libs/service-portal/documents/src/utils/downloadDocumentV2.ts index 3e9748b502ef..980526b7f876 100644 --- a/libs/service-portal/documents/src/utils/downloadDocumentV2.ts +++ b/libs/service-portal/documents/src/utils/downloadDocumentV2.ts @@ -1,6 +1,39 @@ import { User } from '@island.is/auth/react' import { ActiveDocumentType2 } from '../lib/types' +export const sendForm = async (id: string, url: string, userInfo: User) => { + // Create form elements + const form = document.createElement('form') + const documentIdInput = document.createElement('input') + const tokenInput = document.createElement('input') + + const token = userInfo?.access_token + + if (!token) return + + form.appendChild(documentIdInput) + form.appendChild(tokenInput) + + // Form values + form.method = 'post' + form.action = url + form.target = '_blank' + + // Document Id values + documentIdInput.type = 'hidden' + documentIdInput.name = 'documentId' + documentIdInput.value = id + + // National Id values + tokenInput.type = 'hidden' + tokenInput.name = '__accessToken' + tokenInput.value = token + + document.body.appendChild(form) + form.submit() + document.body.removeChild(form) +} + export const downloadFile = async ( doc: ActiveDocumentType2, userInfo: User, diff --git a/libs/service-portal/law-and-order/.babelrc b/libs/service-portal/law-and-order/.babelrc new file mode 100644 index 000000000000..1ea870ead410 --- /dev/null +++ b/libs/service-portal/law-and-order/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@nx/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [] +} diff --git a/libs/service-portal/law-and-order/.eslintrc.json b/libs/service-portal/law-and-order/.eslintrc.json new file mode 100644 index 000000000000..75b85077debb --- /dev/null +++ b/libs/service-portal/law-and-order/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["plugin:@nx/react", "../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/service-portal/law-and-order/README.md b/libs/service-portal/law-and-order/README.md new file mode 100644 index 000000000000..e8dba631e6e3 --- /dev/null +++ b/libs/service-portal/law-and-order/README.md @@ -0,0 +1,7 @@ +# service-portal-law-and-order + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test service-portal-law-and-order` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/service-portal/law-and-order/codegen.yml b/libs/service-portal/law-and-order/codegen.yml new file mode 100644 index 000000000000..802e5dd8ddad --- /dev/null +++ b/libs/service-portal/law-and-order/codegen.yml @@ -0,0 +1,18 @@ +schema: + - apps/api/src/api.graphql +documents: + - libs/service-portal/law-and-order/src/**/**/*.graphql +generates: + libs/service-portal/law-and-order/src/: + preset: 'near-operation-file' + presetConfig: + baseTypesPath: '~@island.is/api/schema' + plugins: + - typescript-operations + - typescript-react-apollo + config: + scalars: + DateTime: string +hooks: + afterAllFileWrite: + - prettier --write diff --git a/libs/service-portal/law-and-order/jest.config.ts b/libs/service-portal/law-and-order/jest.config.ts new file mode 100644 index 000000000000..a9f362d6ae79 --- /dev/null +++ b/libs/service-portal/law-and-order/jest.config.ts @@ -0,0 +1,11 @@ +/* eslint-disable */ +export default { + displayName: 'service-portal-law-and-order', + preset: '../../../jest.preset.js', + transform: { + '^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest', + '^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/react/babel'] }], + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: '../../../coverage/libs/service-portal/law-and-order', +} diff --git a/libs/service-portal/law-and-order/project.json b/libs/service-portal/law-and-order/project.json new file mode 100644 index 000000000000..9a1d710f440e --- /dev/null +++ b/libs/service-portal/law-and-order/project.json @@ -0,0 +1,38 @@ +{ + "name": "service-portal-law-and-order", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/service-portal/law-and-order/src", + "projectType": "library", + "targets": { + "lint": { + "executor": "@nx/linter:eslint", + "options": { + "lintFilePatterns": [ + "libs/service-portal/law-and-order/**/*.{ts,tsx,js,jsx}" + ] + } + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/service-portal/law-and-order/jest.config.ts", + "passWithNoTests": true + } + }, + "extract-strings": { + "executor": "nx:run-commands", + "options": { + "command": "yarn ts-node -P libs/localization/tsconfig.lib.json libs/localization/scripts/extract 'libs/service-portal/law-and-order/src/{lib,screens,components}/**/*.{js,ts,tsx}'" + } + }, + "codegen/frontend-client": { + "executor": "nx:run-commands", + "options": { + "output": "libs/service-portal/law-and-order/src/**/*.generated.ts", + "command": "graphql-codegen --config libs/service-portal/law-and-order/codegen.yml" + } + } + }, + "tags": ["lib:portals-mypages", "scope:portals-mypages"] +} diff --git a/libs/service-portal/law-and-order/src/components/DefenderChoices/DefenderChoices.tsx b/libs/service-portal/law-and-order/src/components/DefenderChoices/DefenderChoices.tsx new file mode 100644 index 000000000000..7779fc0a2c65 --- /dev/null +++ b/libs/service-portal/law-and-order/src/components/DefenderChoices/DefenderChoices.tsx @@ -0,0 +1,248 @@ +import { + Box, + Text, + Button, + Stack, + RadioButton, + toast, + GridColumn, + LoadingDots, +} from '@island.is/island-ui/core' +import { useLocale, useNamespaces } from '@island.is/localization' +import { Controller, FormProvider, useForm } from 'react-hook-form' +import { Problem } from '@island.is/react-spa/shared' +import { SelectController } from '@island.is/shared/form-fields' +import { messages } from '../../lib/messages' +import { Dispatch, FC, SetStateAction, useEffect } from 'react' +import { + useGetLawyersQuery, + usePostDefenseChoiceMutation, +} from './Lawyers.generated' +import { LawAndOrderDefenseChoiceEnum } from '@island.is/api/schema' + +interface Props { + id: string + popUp?: { + setPopUp: Dispatch> + } + refetch?: () => void + choice?: LawAndOrderDefenseChoiceEnum | null +} + +interface FormData { + caseId: string + choice: LawAndOrderDefenseChoiceEnum + lawyersNationalId?: string +} + +const DefenderChoices: FC> = ({ + popUp, + id, + refetch, + choice, +}) => { + useNamespaces('sp.law-and-order') + const { formatMessage, lang } = useLocale() + const { data, loading, error } = useGetLawyersQuery({ + variables: { locale: lang }, + }) + + const lawyers = data?.lawAndOrderLawyers?.lawyers + const choices = data?.lawAndOrderLawyers?.choices + + const methods = useForm() + + const NATIONAL_ID = 'lawyersNationalId' + const CHOICE = 'choice' + + // Choice should default to "delay" if no choice is present in the data + // We want to set the value in order to not have the form data undefined if user doesnt change from pre-selected value + useEffect(() => { + if (methods.formState.isSubmitting || methods.formState.isSubmitted) return + methods.setValue(CHOICE, choice ?? LawAndOrderDefenseChoiceEnum.DELAY) + }) + + const [postAction, { loading: postActionLoading }] = + usePostDefenseChoiceMutation({ + onError: () => { + toast.error(formatMessage(messages.registrationError)) + methods.setError(CHOICE, { + message: formatMessage(messages.registrationError), + }) + }, + onCompleted: () => { + popUp && popUp.setPopUp(false) + toast.success(formatMessage(messages.registrationCompleted)) + refetch && refetch() + }, + }) + + const handleSubmitForm = (data: FormData) => { + postAction({ + variables: { + input: { + caseId: id, + choice: data.choice ?? LawAndOrderDefenseChoiceEnum.DELAY, + lawyersNationalId: data.lawyersNationalId, + }, + locale: lang, + }, + }) + } + + const clearLawyersNationalId = () => { + methods.setValue(NATIONAL_ID, undefined) + } + + return ( + + {popUp ? ( + + {formatMessage(messages.chooseDefenderTitle)} + + ) : ( + + {formatMessage(messages.chooseDefenderTitle)} + + )} + {loading && !error && } + {!loading && error && } + {lawyers === null ? ( + + ) : ( + +
+ ( + + {choices?.map((item) => ( + <> + { + onChange(target.value) + clearLawyersNationalId() + }} + /> + {item.id === LawAndOrderDefenseChoiceEnum.CHOOSE && + value === LawAndOrderDefenseChoiceEnum.CHOOSE && + loading && + !lawyers && } + {lawyers && + item.id === LawAndOrderDefenseChoiceEnum.CHOOSE && + value === LawAndOrderDefenseChoiceEnum.CHOOSE && ( + + { + return { + label: x.title ?? '', + value: x.nationalId, + } + })} + onSelect={(selected) => { + methods.setValue( + CHOICE, + LawAndOrderDefenseChoiceEnum.CHOOSE, + ) + selected.value && + methods.setValue(NATIONAL_ID, selected?.value) + }} + error={ + methods.getValues().lawyersNationalId === + undefined + ? formatMessage(messages.pleaseChooseALawyer) + : undefined + } + required + /> + + )} + + ))} + + {!popUp && ( + + )} + {popUp && ( + + + + + + + + + )} + + )} + /> + +
+ )} +
+ ) +} + +export default DefenderChoices diff --git a/libs/service-portal/law-and-order/src/components/DefenderChoices/Lawyers.graphql b/libs/service-portal/law-and-order/src/components/DefenderChoices/Lawyers.graphql new file mode 100644 index 000000000000..1c71f698c9d4 --- /dev/null +++ b/libs/service-portal/law-and-order/src/components/DefenderChoices/Lawyers.graphql @@ -0,0 +1,21 @@ +query GetLawyers($locale: String!) { + lawAndOrderLawyers(locale: $locale) { + lawyers { + title + nationalId + } + choices { + id + label + } + } +} + +mutation PostDefenseChoice( + $input: LawAndOrderDefenseChoiceInput! + $locale: String! +) { + lawAndOrderDefenseChoicePost(input: $input, locale: $locale) { + caseId + } +} diff --git a/libs/service-portal/law-and-order/src/components/InfoLines/InfoLines.tsx b/libs/service-portal/law-and-order/src/components/InfoLines/InfoLines.tsx new file mode 100644 index 000000000000..175adc550031 --- /dev/null +++ b/libs/service-portal/law-and-order/src/components/InfoLines/InfoLines.tsx @@ -0,0 +1,82 @@ +import React from 'react' + +import { Box, Divider, Link, Stack, Text } from '@island.is/island-ui/core' +import { useNamespaces } from '@island.is/localization' +import { InfoLine } from '@island.is/service-portal/core' +import { + LawAndOrderActionTypeEnum, + LawAndOrderGroup, +} from '@island.is/api/schema' + +interface Props { + groups: Array + loading?: boolean +} + +const InfoLines: React.FC> = (props) => { + useNamespaces('sp.law-and-order') + + return ( + + {props.groups.map((x) => { + return ( + <> + + {x.items?.map((y, i) => { + return ( + <> + {x.label && i === 0 && ( + + {x.label} + + )} + + y.link ? ( + + {y.value} + + ) : ( + {y.value} + ) + } + button={ + y.action?.type === LawAndOrderActionTypeEnum.url && + y.action?.title && + y.action.data + ? { + type: 'link', + to: y.action?.data, + label: y.action?.title, + icon: 'arrowForward', + } + : undefined + } + /> + + + ) + })} + + ) + })} + + ) +} + +export default InfoLines diff --git a/libs/service-portal/law-and-order/src/index.ts b/libs/service-portal/law-and-order/src/index.ts new file mode 100644 index 000000000000..aea4d6e43879 --- /dev/null +++ b/libs/service-portal/law-and-order/src/index.ts @@ -0,0 +1,2 @@ +export * from './module' +export * from './lib/navigation' diff --git a/libs/service-portal/law-and-order/src/lib/messages.ts b/libs/service-portal/law-and-order/src/lib/messages.ts new file mode 100644 index 000000000000..b9c349c33814 --- /dev/null +++ b/libs/service-portal/law-and-order/src/lib/messages.ts @@ -0,0 +1,109 @@ +import { defineMessages } from 'react-intl' + +export const messages = defineMessages({ + myData: { + id: 'sp.law-and-order:my-data', + defaultMessage: 'Mín gögn', + }, + courtCases: { + id: 'sp.law-and-order:court-cases', + defaultMessage: 'Dómsmál', + }, + courtCasesDescription: { + id: 'sp.law-and-order:court-cases-desc', + defaultMessage: 'Hér finnur þú mál sem þú átt aðild að í dómskerfinu.', + }, + seeInfo: { + id: 'sp.law-and-order:see-info', + defaultMessage: 'Skoða nánar', + }, + courtCaseNotFound: { + id: 'sp.law-and-order:courtcase-not-found', + defaultMessage: 'Dómsmál fannst ekki', + }, + courtCaseNumberNotRegistered: { + id: 'sp.law-and-order:courtcasenumber-not-registered', + defaultMessage: 'Málsnúmer hefur ekki verið skráð', + }, + defendant: { + id: 'sp.law-and-order:defendant', + defaultMessage: 'Varnaraðili', + }, + subpoena: { + id: 'sp.law-and-order:subpoena', + defaultMessage: 'Fyrirkall', + }, + subpoenaSent: { + id: 'sp.law-and-order:subpoena-sent', + defaultMessage: 'Fyrirkall sent', + }, + seeSubpoena: { + id: 'sp.law-and-order:see-subpoena', + defaultMessage: 'Sjá fyrirkall', + }, + subpoenaNotFound: { + id: 'sp.law-and-order:subpoena-not-found', + defaultMessage: 'Fyrirkall fannst ekki', + }, + chooseDefenderTitle: { + id: 'sp.law-and-order:choose-defender-title', + defaultMessage: 'Val á verjanda', + }, + defenderList: { + id: 'sp.law-and-order:defender-list', + defaultMessage: 'Listi af verjendum', + }, + chooseDefender: { + id: 'sp.law-and-order:choose-defender', + defaultMessage: 'Veldu verjanda', + }, + defenderLimits: { + id: 'sp.law-and-order:defender-limits', + defaultMessage: 'Það er hægt að breyta vali á verjanda í eitt skipti.', + }, + date: { + id: 'sp.law-and-order:date', + defaultMessage: 'Dagsetning', + }, + openSubpoena: { + id: 'sp.law-and-order:open-subpoena', + defaultMessage: 'Opna erindi', + }, + confirm: { + id: 'sp.law-and-order:confirm', + defaultMessage: 'Staðfesta', + }, + cancel: { + id: 'sp.law-and-order:cancel', + defaultMessage: 'Hætta við', + }, + registrationCompleted: { + id: 'sp.law-and-order:registration-completed', + defaultMessage: 'Skráning tókst', + }, + registrationError: { + id: 'sp.law-and-order:registration-error', + defaultMessage: 'Skráning tókst ekki', + }, + change: { + id: 'sp.law-and-order:change', + defaultMessage: 'Breyta', + }, + pleaseChooseALawyer: { + id: 'sp.law-and-order:please-choose-a-lawyer', + defaultMessage: 'Vinsamlegast veljið verjanda', + }, + goBackToCourtCase: { + id: 'sp.law-and-order:go-back-to-court-case', + defaultMessage: 'Til baka í dómsmál', + }, + subpoenaInfoText: { + id: 'sp.law-and-order:subpoena-info-text', + defaultMessage: + 'Ákærði er kvaddur til að koma fyrir dóm, hlýða á ákæru, halda uppi vörnum og sæta dómi. Sæki ákærði ekki þing má hann búast við því að fjarvist hans verði metin til jafns við það að hann viðurkenni að hafa framið brot það sem hann er ákærður fyrir og dómur verði lagður á málið að honum fjarstöddum.', + }, + subpoenaInfoText2: { + id: 'sp.law-and-order:subpoena-info-text2', + defaultMessage: 'Birtingarfrestur er þrír sólarhringar.', + }, +}) diff --git a/libs/service-portal/law-and-order/src/lib/navigation.ts b/libs/service-portal/law-and-order/src/lib/navigation.ts new file mode 100644 index 000000000000..f2e8ec031a51 --- /dev/null +++ b/libs/service-portal/law-and-order/src/lib/navigation.ts @@ -0,0 +1,37 @@ +import { PortalNavigationItem } from '@island.is/portals/core' +import { m } from '@island.is/service-portal/core' +import { LawAndOrderPaths } from './paths' + +export const lawAndOrderNavigation: PortalNavigationItem = { + name: m.lawAndOrder, + description: m.lawAndOrderDashboard, + path: LawAndOrderPaths.Root, + icon: { + icon: 'attach', + }, + children: [ + { + name: m.overview, + path: LawAndOrderPaths.Overview, + }, + { + name: m.courtCases, + path: LawAndOrderPaths.CourtCases, + breadcrumbHide: true, + children: [ + { + name: m.courtCases, + path: LawAndOrderPaths.CourtCaseDetail, + navHide: true, + children: [ + { + name: m.subpoena, + path: LawAndOrderPaths.SubpoenaDetail, + navHide: true, + }, + ], + }, + ], + }, + ], +} diff --git a/libs/service-portal/law-and-order/src/lib/paths.ts b/libs/service-portal/law-and-order/src/lib/paths.ts new file mode 100644 index 000000000000..91ff2d7c9c35 --- /dev/null +++ b/libs/service-portal/law-and-order/src/lib/paths.ts @@ -0,0 +1,8 @@ +export enum LawAndOrderPaths { + Root = '/log-og-reglur', + Overview = '/log-og-reglur/yfirlit', + CourtCases = '/log-og-reglur/domsmal', + CourtCaseDetail = '/log-og-reglur/domsmal/:id', + SubpoenaDetail = '/log-og-reglur/domsmal/:id/fyrirkall', + SubpoenaPopUp = '/log-og-reglur/domsmal/:id/fyrirkall/pop-up', +} diff --git a/libs/service-portal/law-and-order/src/module.tsx b/libs/service-portal/law-and-order/src/module.tsx new file mode 100644 index 000000000000..6de5049bfa00 --- /dev/null +++ b/libs/service-portal/law-and-order/src/module.tsx @@ -0,0 +1,50 @@ +import { PortalModule } from '@island.is/portals/core' +import { LawAndOrderPaths } from './lib/paths' +import { ApiScope } from '@island.is/auth/scopes' +import { Navigate } from 'react-router-dom' +import { m } from '@island.is/service-portal/core' +import { lazy } from 'react' +import { Features } from '@island.is/feature-flags' + +const Overview = lazy(() => import('./screens/Overview/LawAndOrderOverview')) +const CourtCases = lazy(() => import('./screens/CourtCases/CourtCases')) +const CourtCaseDetail = lazy(() => + import('./screens/CourtCaseDetail/CourtCaseDetail'), +) +const Subpoena = lazy(() => import('./screens/Subpoena/Subpoena')) +export const lawAndOrderModule: PortalModule = { + name: m.lawAndOrder, + featureFlag: Features.servicePortalLawAndOrderModuleEnabled, + routes: ({ userInfo }) => [ + { + name: m.lawAndOrder, + path: LawAndOrderPaths.Root, + enabled: userInfo.scopes.includes(ApiScope.lawAndOrder), + element: , + }, + { + name: m.overview, + path: LawAndOrderPaths.Overview, + enabled: userInfo.scopes.includes(ApiScope.lawAndOrder), + element: , + }, + { + name: m.courtCases, + path: LawAndOrderPaths.CourtCases, + enabled: userInfo.scopes.includes(ApiScope.lawAndOrder), + element: , + }, + { + name: m.courtCases, + path: LawAndOrderPaths.CourtCaseDetail, + enabled: userInfo.scopes.includes(ApiScope.lawAndOrder), + element: , + }, + { + name: m.subpoena, + path: LawAndOrderPaths.SubpoenaDetail, + enabled: userInfo.scopes.includes(ApiScope.lawAndOrder), + element: , + }, + ], +} diff --git a/libs/service-portal/law-and-order/src/screens/CourtCaseDetail/CourtCaseDetail.graphql b/libs/service-portal/law-and-order/src/screens/CourtCaseDetail/CourtCaseDetail.graphql new file mode 100644 index 000000000000..7a76d0cb40b9 --- /dev/null +++ b/libs/service-portal/law-and-order/src/screens/CourtCaseDetail/CourtCaseDetail.graphql @@ -0,0 +1,26 @@ +query GetCourtCase($input: LawAndOrderCourtCaseInput!, $locale: String!) { + lawAndOrderCourtCaseDetail(input: $input, locale: $locale) { + texts { + intro + footnote + } + data { + id + hasBeenServed + caseNumberTitle + groups { + label + items { + label + value + link + action { + type + title + data + } + } + } + } + } +} diff --git a/libs/service-portal/law-and-order/src/screens/CourtCaseDetail/CourtCaseDetail.tsx b/libs/service-portal/law-and-order/src/screens/CourtCaseDetail/CourtCaseDetail.tsx new file mode 100644 index 000000000000..d1dd6ae59674 --- /dev/null +++ b/libs/service-portal/law-and-order/src/screens/CourtCaseDetail/CourtCaseDetail.tsx @@ -0,0 +1,92 @@ +import { Box } from '@island.is/island-ui/core' +import { + DOMSMALARADUNEYTID_SLUG, + IntroHeader, + LinkButton, + m, +} from '@island.is/service-portal/core' +import { messages } from '../../lib/messages' +import { useLocale, useNamespaces } from '@island.is/localization' +import { useParams } from 'react-router-dom' +import { LawAndOrderPaths } from '../../lib/paths' +import InfoLines from '../../components/InfoLines/InfoLines' +import { useEffect } from 'react' +import { useGetCourtCaseQuery } from './CourtCaseDetail.generated' +import { Problem } from '@island.is/react-spa/shared' + +type UseParams = { + id: string +} + +const CourtCaseDetail = () => { + useNamespaces('sp.law-and-order') + const { formatMessage, lang } = useLocale() + + const { id } = useParams() as UseParams + + const { data, error, loading, refetch } = useGetCourtCaseQuery({ + variables: { + input: { + id, + }, + locale: lang, + }, + }) + + const courtCase = data?.lawAndOrderCourtCaseDetail + + useEffect(() => { + refetch() + }, [lang]) + + return ( + <> + + + {data?.lawAndOrderCourtCaseDetail && !loading && ( + + {courtCase?.data?.hasBeenServed && ( + + )} + + )} + + {error && !loading && } + {!error && courtCase && courtCase?.data?.groups && ( + + )} + + {!loading && + !error && + courtCase?.data && + courtCase?.data?.groups?.length === 0 && ( + + )} + + ) +} +export default CourtCaseDetail diff --git a/libs/service-portal/law-and-order/src/screens/CourtCases/CourtCases.graphql b/libs/service-portal/law-and-order/src/screens/CourtCases/CourtCases.graphql new file mode 100644 index 000000000000..38fbc4691776 --- /dev/null +++ b/libs/service-portal/law-and-order/src/screens/CourtCases/CourtCases.graphql @@ -0,0 +1,13 @@ +query GetCourtCases($locale: String!) { + lawAndOrderCourtCasesList(locale: $locale) { + cases { + id + caseNumberTitle + type + state { + label + color + } + } + } +} diff --git a/libs/service-portal/law-and-order/src/screens/CourtCases/CourtCases.tsx b/libs/service-portal/law-and-order/src/screens/CourtCases/CourtCases.tsx new file mode 100644 index 000000000000..6ae1e1ecf6fb --- /dev/null +++ b/libs/service-portal/law-and-order/src/screens/CourtCases/CourtCases.tsx @@ -0,0 +1,86 @@ +import { Box, TagVariant } from '@island.is/island-ui/core' +import { + ActionCard, + CardLoader, + DOMSMALARADUNEYTID_SLUG, + IntroHeader, + m, +} from '@island.is/service-portal/core' +import { messages } from '../../lib/messages' +import { useLocale, useNamespaces } from '@island.is/localization' +import { LawAndOrderPaths } from '../../lib/paths' +import { useGetCourtCasesQuery } from './CourtCases.generated' +import { Problem } from '@island.is/react-spa/shared' +import { useEffect } from 'react' + +const CourtCases = () => { + useNamespaces('sp.law-and-order') + const { lang } = useLocale() + const { formatMessage } = useLocale() + + const { data, loading, error, refetch } = useGetCourtCasesQuery({ + variables: { + locale: lang, + }, + }) + + const cases = data?.lawAndOrderCourtCasesList?.cases + + useEffect(() => { + refetch() + }, [lang]) + + return ( + <> + + {loading && !error && ( + + + + )} + + {error && !loading && } + + {!loading && + cases && + cases.length > 0 && + cases.map((x) => ( + + + + ))} + {!loading && !error && cases?.length === 0 && ( + + )} + + ) +} +export default CourtCases diff --git a/libs/service-portal/law-and-order/src/screens/Overview/LawAndOrderOverview.tsx b/libs/service-portal/law-and-order/src/screens/Overview/LawAndOrderOverview.tsx new file mode 100644 index 000000000000..57668f7ac368 --- /dev/null +++ b/libs/service-portal/law-and-order/src/screens/Overview/LawAndOrderOverview.tsx @@ -0,0 +1,41 @@ +import { Box, Divider, Text } from '@island.is/island-ui/core' +import { + DOMSMALARADUNEYTID_SLUG, + IntroHeader, + m, + InfoLine, +} from '@island.is/service-portal/core' +import { messages } from '../../lib/messages' +import { useLocale, useNamespaces } from '@island.is/localization' +import { LawAndOrderPaths } from '../../lib/paths' + +const LawAndOrderOverview = () => { + useNamespaces('sp.law-and-order') + const { formatMessage } = useLocale() + return ( + <> + + + + {formatMessage(messages.myData)} + + + + + + ) +} +export default LawAndOrderOverview diff --git a/libs/service-portal/law-and-order/src/screens/Subpoena/Subpoena.graphql b/libs/service-portal/law-and-order/src/screens/Subpoena/Subpoena.graphql new file mode 100644 index 000000000000..925a2a8e4023 --- /dev/null +++ b/libs/service-portal/law-and-order/src/screens/Subpoena/Subpoena.graphql @@ -0,0 +1,28 @@ +query GetSubpoena($input: LawAndOrderSubpoenaInput!, $locale: String!) { + lawAndOrderSubpoena(input: $input, locale: $locale) { + data { + id + hasBeenServed + chosenDefender + defenderChoice + canEditDefenderChoice + courtContactInfo + groups { + label + items { + label + value + link + } + } + alerts { + type + message + } + } + texts { + confirmation + description + } + } +} diff --git a/libs/service-portal/law-and-order/src/screens/Subpoena/Subpoena.tsx b/libs/service-portal/law-and-order/src/screens/Subpoena/Subpoena.tsx new file mode 100644 index 000000000000..605062c6166e --- /dev/null +++ b/libs/service-portal/law-and-order/src/screens/Subpoena/Subpoena.tsx @@ -0,0 +1,173 @@ +import { + AlertMessage, + Box, + Button, + Divider, + Text, +} from '@island.is/island-ui/core' +import { + DOMSMALARADUNEYTID_SLUG, + InfoLine, + IntroHeader, + LinkResolver, + m, + Modal, +} from '@island.is/service-portal/core' +import { useLocale, useNamespaces } from '@island.is/localization' +import { isDefined } from '@island.is/shared/utils' +import { Problem } from '@island.is/react-spa/shared' +import { Navigate, useParams } from 'react-router-dom' +import { useState } from 'react' +import InfoLines from '../../components/InfoLines/InfoLines' +import DefenderChoices from '../../components/DefenderChoices/DefenderChoices' +import { useGetSubpoenaQuery } from './Subpoena.generated' +import { LawAndOrderPaths } from '../../lib/paths' +import { messages } from '../../lib/messages' + +type UseParams = { + id: string +} + +const Subpoena = () => { + useNamespaces('sp.law-and-order') + const { formatMessage, lang } = useLocale() + const { id } = useParams() as UseParams + + const { data, error, loading, refetch } = useGetSubpoenaQuery({ + variables: { + input: { + id, + }, + locale: lang, + }, + }) + + const subpoena = data?.lawAndOrderSubpoena + const [defenderPopUp, setDefenderPopUp] = useState(false) + + if ( + subpoena?.data && + (!isDefined(subpoena.data.hasBeenServed) || + subpoena.data.hasBeenServed === false) + ) { + return + } + + return ( + <> + + + + ) + } + /> + + + {!loading && subpoena?.data && ( + + + + + + )} + + + {error && !loading && } + + {subpoena?.data?.groups && subpoena.data.groups.length > 0 && ( + <> + + {isDefined(subpoena?.data.defenderChoice) && ( + <> + + { + setDefenderPopUp(true) + }, + disabled: !subpoena.data.canEditDefenderChoice, + tooltip: subpoena.data.canEditDefenderChoice + ? undefined + : subpoena.data.courtContactInfo ?? '', + }} + /> + + + + )} + + + {formatMessage(messages.subpoenaInfoText)} + + {formatMessage(messages.subpoenaInfoText2)} + + + {!loading && !subpoena?.data.defenderChoice && ( + + )} + + {defenderPopUp && ( + setDefenderPopUp(false)} + > + + + )} + + )} + {!loading && !error && subpoena?.data?.groups?.length === 0 && ( + + )} + + ) +} + +export default Subpoena diff --git a/libs/service-portal/law-and-order/tsconfig.json b/libs/service-portal/law-and-order/tsconfig.json new file mode 100644 index 000000000000..4daaf45cd328 --- /dev/null +++ b/libs/service-portal/law-and-order/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": false, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../../tsconfig.base.json" +} diff --git a/libs/service-portal/law-and-order/tsconfig.lib.json b/libs/service-portal/law-and-order/tsconfig.lib.json new file mode 100644 index 000000000000..cec257bd9d59 --- /dev/null +++ b/libs/service-portal/law-and-order/tsconfig.lib.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": ["node"] + }, + "exclude": [ + "jest.config.ts", + "src/**/*.spec.ts", + "src/**/*.test.ts", + "src/**/*.spec.tsx", + "src/**/*.test.tsx", + "src/**/*.spec.js", + "src/**/*.test.js", + "src/**/*.spec.jsx", + "src/**/*.test.jsx" + ], + "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"] +} diff --git a/libs/service-portal/law-and-order/tsconfig.spec.json b/libs/service-portal/law-and-order/tsconfig.spec.json new file mode 100644 index 000000000000..25b7af8f6d00 --- /dev/null +++ b/libs/service-portal/law-and-order/tsconfig.spec.json @@ -0,0 +1,20 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts" + ] +} diff --git a/libs/shared/form-fields/src/lib/SelectController/SelectController.tsx b/libs/shared/form-fields/src/lib/SelectController/SelectController.tsx index 7f8e71f72405..9a0e6bb863bb 100644 --- a/libs/shared/form-fields/src/lib/SelectController/SelectController.tsx +++ b/libs/shared/form-fields/src/lib/SelectController/SelectController.tsx @@ -20,6 +20,7 @@ interface SelectControllerProps { ) => void backgroundColor?: InputBackgroundColor isSearchable?: boolean + isClearable?: boolean isMulti?: IsMulti required?: boolean rules?: RegisterOptions @@ -40,6 +41,7 @@ export const SelectController = ({ backgroundColor, isSearchable, isMulti, + isClearable = false, dataTestId, required = false, rules, @@ -86,6 +88,7 @@ export const SelectController = ({ value={getValue(value)} isSearchable={isSearchable} isMulti={isMulti} + isClearable={isClearable} size={size} // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore make web strict diff --git a/tsconfig.base.json b/tsconfig.base.json index 6b2c06c67d84..36a85729ce40 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -146,6 +146,9 @@ "@island.is/api/domains/islykill": [ "libs/api/domains/islykill/src/index.ts" ], + "@island.is/api/domains/law-and-order": [ + "libs/api/domains/law-and-order/src/index.ts" + ], "@island.is/api/domains/license-service": [ "libs/api/domains/license-service/src/index.ts" ], @@ -694,6 +697,9 @@ "@island.is/clients/judicial-administration": [ "libs/clients/judicial-administration/src/index.ts" ], + "@island.is/clients/judicial-system-sp": [ + "libs/clients/judicial-system-sp/src/index.ts" + ], "@island.is/clients/license-client": [ "libs/clients/license-client/src/index.ts" ], @@ -1049,6 +1055,9 @@ "@island.is/service-portal/information/messages": [ "libs/service-portal/information/src/lib/messages.ts" ], + "@island.is/service-portal/law-and-order": [ + "libs/service-portal/law-and-order/src/index.ts" + ], "@island.is/service-portal/licenses": [ "libs/service-portal/licenses/src/index.ts" ],