From 8b2258453c22b6c292ac99124e06aa1ad361fffe Mon Sep 17 00:00:00 2001 From: veronikasif <54938148+veronikasif@users.noreply.github.com> Date: Fri, 13 Sep 2024 14:18:32 +0000 Subject: [PATCH 01/27] feat(income-plan): Add disabled to TableRepeaterItem (#16001) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../income-plan/src/forms/IncomePlanForm.ts | 2 +- libs/application/types/src/lib/Fields.ts | 6 ++++++ .../src/lib/TableRepeaterFormField/TableRepeaterItem.tsx | 9 +++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/libs/application/templates/social-insurance-administration/income-plan/src/forms/IncomePlanForm.ts b/libs/application/templates/social-insurance-administration/income-plan/src/forms/IncomePlanForm.ts index e48b985bcfe9..1816df2d3156 100644 --- a/libs/application/templates/social-insurance-administration/income-plan/src/forms/IncomePlanForm.ts +++ b/libs/application/templates/social-insurance-administration/income-plan/src/forms/IncomePlanForm.ts @@ -266,7 +266,7 @@ export const IncomePlanForm: Form = buildForm({ width: 'half', type: 'number', currency: true, - readonly: (_, activeField) => { + disabled: (_, activeField) => { return activeField?.income === RatioType.MONTHLY }, updateValueObj: { diff --git a/libs/application/types/src/lib/Fields.ts b/libs/application/types/src/lib/Fields.ts index a3631b76dc20..a41aabcde12f 100644 --- a/libs/application/types/src/lib/Fields.ts +++ b/libs/application/types/src/lib/Fields.ts @@ -94,6 +94,12 @@ export type TableRepeaterItem = { application: Application, activeField?: Record, ) => boolean) + disabled?: + | boolean + | (( + application: Application, + activeField?: Record, + ) => boolean) updateValueObj?: { valueModifier: (activeField?: Record) => unknown watchValues: diff --git a/libs/application/ui-fields/src/lib/TableRepeaterFormField/TableRepeaterItem.tsx b/libs/application/ui-fields/src/lib/TableRepeaterFormField/TableRepeaterItem.tsx index 0d602aea5f53..194df3dd02bf 100644 --- a/libs/application/ui-fields/src/lib/TableRepeaterFormField/TableRepeaterItem.tsx +++ b/libs/application/ui-fields/src/lib/TableRepeaterFormField/TableRepeaterItem.tsx @@ -54,6 +54,7 @@ export const Item = ({ width = 'full', condition, readonly = false, + disabled = false, updateValueObj, defaultValue, ...props @@ -148,6 +149,13 @@ export const Item = ({ Readonly = readonly } + let Disabled: boolean | undefined + if (typeof disabled === 'function') { + Disabled = disabled(application, activeValues) + } else { + Disabled = disabled + } + let DefaultValue: any if (component === 'input') { DefaultValue = getDefaultValue(item, application, activeValues) @@ -194,6 +202,7 @@ export const Item = ({ error={getFieldError(itemId)} control={control} readOnly={Readonly} + disabled={Disabled} backgroundColor={backgroundColor} onChange={() => { if (error) { From f257ea7ce3e77489e8156a5e6b7106cb10c33362 Mon Sep 17 00:00:00 2001 From: veronikasif <54938148+veronikasif@users.noreply.github.com> Date: Fri, 13 Sep 2024 14:33:21 +0000 Subject: [PATCH 02/27] feat(income-plan): Update updateValueObj in incomeType (#15999) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../income-plan/src/forms/IncomePlanForm.ts | 21 ++++++++++++++----- libs/application/types/src/lib/Fields.ts | 5 ++++- .../TableRepeaterItem.tsx | 5 ++++- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/libs/application/templates/social-insurance-administration/income-plan/src/forms/IncomePlanForm.ts b/libs/application/templates/social-insurance-administration/income-plan/src/forms/IncomePlanForm.ts index 1816df2d3156..4e125542632d 100644 --- a/libs/application/templates/social-insurance-administration/income-plan/src/forms/IncomePlanForm.ts +++ b/libs/application/templates/social-insurance-administration/income-plan/src/forms/IncomePlanForm.ts @@ -113,7 +113,18 @@ export const IncomePlanForm: Form = buildForm({ width: 'half', isSearchable: true, updateValueObj: { - valueModifier: (_) => '', + valueModifier: (application, activeField) => { + const options = getTypesOptions( + application.externalData, + activeField?.incomeCategory, + ) + + return ( + options.find( + (option) => option.value === activeField?.incomeType, + )?.value ?? '' + ) + }, watchValues: 'incomeCategory', }, options: (application, activeField) => { @@ -129,7 +140,7 @@ export const IncomePlanForm: Form = buildForm({ placeholder: incomePlanFormMessage.incomePlan.selectCurrency, isSearchable: true, updateValueObj: { - valueModifier: (activeField) => { + valueModifier: (_, activeField) => { const defaultCurrency = activeField?.incomeType === FOREIGN_BASIC_PENSION || activeField?.incomeType === FOREIGN_PENSION || @@ -186,7 +197,7 @@ export const IncomePlanForm: Form = buildForm({ displayInTable: false, currency: true, updateValueObj: { - valueModifier: (activeField) => { + valueModifier: (_, activeField) => { const unevenAndEmploymentIncome = activeField?.unevenIncomePerYear?.[0] !== YES || (activeField?.incomeCategory !== INCOME && @@ -227,7 +238,7 @@ export const IncomePlanForm: Form = buildForm({ displayInTable: false, currency: true, updateValueObj: { - valueModifier: (activeField) => { + valueModifier: (_, activeField) => { const unevenAndEmploymentIncome = activeField?.unevenIncomePerYear?.[0] !== YES || (activeField?.incomeCategory !== INCOME && @@ -270,7 +281,7 @@ export const IncomePlanForm: Form = buildForm({ return activeField?.income === RatioType.MONTHLY }, updateValueObj: { - valueModifier: (activeField) => { + valueModifier: (_, activeField) => { if ( activeField?.income === RatioType.MONTHLY && activeField?.incomeCategory === INCOME && diff --git a/libs/application/types/src/lib/Fields.ts b/libs/application/types/src/lib/Fields.ts index a41aabcde12f..ce69f6a82d40 100644 --- a/libs/application/types/src/lib/Fields.ts +++ b/libs/application/types/src/lib/Fields.ts @@ -101,7 +101,10 @@ export type TableRepeaterItem = { activeField?: Record, ) => boolean) updateValueObj?: { - valueModifier: (activeField?: Record) => unknown + valueModifier: ( + application: Application, + activeField?: Record, + ) => unknown watchValues: | string | string[] diff --git a/libs/application/ui-fields/src/lib/TableRepeaterFormField/TableRepeaterItem.tsx b/libs/application/ui-fields/src/lib/TableRepeaterFormField/TableRepeaterItem.tsx index 194df3dd02bf..441f8074ac96 100644 --- a/libs/application/ui-fields/src/lib/TableRepeaterFormField/TableRepeaterItem.tsx +++ b/libs/application/ui-fields/src/lib/TableRepeaterFormField/TableRepeaterItem.tsx @@ -99,7 +99,10 @@ export const Item = ({ ? !watchedValues.every((value) => value === undefined) : true) ) { - const finalValue = updateValueObj.valueModifier(activeValues) + const finalValue = updateValueObj.valueModifier( + application, + activeValues, + ) setValue(id, finalValue) } } From a81168087b244662a82673b4e5701842446ca2e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dvar=20Oddsson?= Date: Fri, 13 Sep 2024 14:43:20 +0000 Subject: [PATCH 03/27] chore(j-s): Hide the resend indictment button (#15994) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../routes/Court/Indictments/Overview/Overview.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/apps/judicial-system/web/src/routes/Court/Indictments/Overview/Overview.tsx b/apps/judicial-system/web/src/routes/Court/Indictments/Overview/Overview.tsx index 62ba4388fa9d..c3982677eafc 100644 --- a/apps/judicial-system/web/src/routes/Court/Indictments/Overview/Overview.tsx +++ b/apps/judicial-system/web/src/routes/Court/Indictments/Overview/Overview.tsx @@ -141,10 +141,15 @@ const IndictmentOverview = () => { ) } nextButtonText={formatMessage(core.continue)} - actionButtonText={formatMessage(strings.returnIndictmentButtonText)} - actionButtonColorScheme={'destructive'} - actionButtonIsDisabled={!caseHasBeenReceivedByCourt} - onActionButtonClick={() => setModalVisible('RETURN_INDICTMENT')} + /* + The return indictment feature has been removed for the time being but + we want to hold on to the functionality for now, since we are likely + to change this feature in the future. + */ + // actionButtonText={formatMessage(strings.returnIndictmentButtonText)} + // actionButtonColorScheme={'destructive'} + // actionButtonIsDisabled={!caseHasBeenReceivedByCourt} + // onActionButtonClick={() => setModalVisible('RETURN_INDICTMENT')} /> {modalVisible === 'RETURN_INDICTMENT' && ( From dddefda9b01b1f73792165ee83884b672c36a507 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9E=C3=B3r=C3=B0ur=20H?= Date: Fri, 13 Sep 2024 15:01:58 +0000 Subject: [PATCH 04/27] chore(regulations-admin): Update national registry for regulations (#15995) * Add nat reg v3 * remove console log * chore: charts update dirty files * Remove unused * test * cleanup * add missing chart value --------- Co-authored-by: andes-it Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../infra/regulations-admin-backend.ts | 10 ++++++++-- .../regulations-admin-backend/src/app/app.module.ts | 4 ++-- .../draft_regulation/draft_regulation.module.ts | 4 ++-- .../draft_regulation/draft_regulation.service.ts | 12 +++++++----- charts/islandis/values.dev.yaml | 9 +++++---- charts/islandis/values.prod.yaml | 9 +++++---- charts/islandis/values.staging.yaml | 9 +++++---- 7 files changed, 34 insertions(+), 23 deletions(-) diff --git a/apps/services/regulations-admin-backend/infra/regulations-admin-backend.ts b/apps/services/regulations-admin-backend/infra/regulations-admin-backend.ts index 5dbc4a161be7..1af574773a05 100644 --- a/apps/services/regulations-admin-backend/infra/regulations-admin-backend.ts +++ b/apps/services/regulations-admin-backend/infra/regulations-admin-backend.ts @@ -1,5 +1,9 @@ import { service, ServiceBuilder } from '../../../../infra/src/dsl/dsl' -import { Base, Client, NationalRegistry } from '../../../../infra/src/dsl/xroad' +import { + Base, + Client, + NationalRegistryB2C, +} from '../../../../infra/src/dsl/xroad' export const serviceSetup = (): ServiceBuilder<'regulations-admin-backend'> => service('regulations-admin-backend') @@ -25,12 +29,14 @@ export const serviceSetup = (): ServiceBuilder<'regulations-admin-backend'> => '/k8s/api/REGULATIONS_FILE_UPLOAD_KEY_PUBLISH', REGULATIONS_FILE_UPLOAD_KEY_PRESIGNED: '/k8s/api/REGULATIONS_FILE_UPLOAD_KEY_PRESIGNED', + NATIONAL_REGISTRY_B2C_CLIENT_SECRET: + '/k8s/api/NATIONAL_REGISTRY_B2C_CLIENT_SECRET', }) .resources({ limits: { cpu: '400m', memory: '512Mi' }, requests: { cpu: '100m', memory: '256Mi' }, }) - .xroad(Base, Client, NationalRegistry) + .xroad(Base, Client, NationalRegistryB2C) .readiness('/liveness') .liveness('/liveness') .grantNamespaces('islandis', 'download-service') diff --git a/apps/services/regulations-admin-backend/src/app/app.module.ts b/apps/services/regulations-admin-backend/src/app/app.module.ts index 9b3091ce8be0..bd954e9eb794 100644 --- a/apps/services/regulations-admin-backend/src/app/app.module.ts +++ b/apps/services/regulations-admin-backend/src/app/app.module.ts @@ -9,7 +9,7 @@ import { AuthModule } from '@island.is/auth-nest-tools' import { AuditModule } from '@island.is/nest/audit' import { environment } from '../environments' -import { NationalRegistryClientConfig } from '@island.is/clients/national-registry-v2' +import { NationalRegistryV3ClientConfig } from '@island.is/clients/national-registry-v3' import { RegulationsClientConfig } from '@island.is/clients/regulations' import { DraftRegulationModule } from './modules/draft_regulation' import { DraftRegulationChangeModule } from './modules/draft_regulation_change' @@ -31,7 +31,7 @@ import { SequelizeConfigService } from './sequelizeConfig.service' load: [ RegulationsClientConfig, XRoadConfig, - NationalRegistryClientConfig, + NationalRegistryV3ClientConfig, IdsClientConfig, ], }), diff --git a/apps/services/regulations-admin-backend/src/app/modules/draft_regulation/draft_regulation.module.ts b/apps/services/regulations-admin-backend/src/app/modules/draft_regulation/draft_regulation.module.ts index 775dbc200d59..841cc4acac67 100644 --- a/apps/services/regulations-admin-backend/src/app/modules/draft_regulation/draft_regulation.module.ts +++ b/apps/services/regulations-admin-backend/src/app/modules/draft_regulation/draft_regulation.module.ts @@ -7,7 +7,7 @@ import { DraftRegulationModel } from './draft_regulation.model' import { DraftRegulationChangeModule } from '../draft_regulation_change' import { DraftRegulationCancelModule } from '../draft_regulation_cancel' import { RegulationsService } from '@island.is/clients/regulations' -import { NationalRegistryClientModule } from '@island.is/clients/national-registry-v2' +import { NationalRegistryV3ClientModule } from '@island.is/clients/national-registry-v3' import { DraftAuthorModule } from '../draft_author' @Module({ @@ -16,7 +16,7 @@ import { DraftAuthorModule } from '../draft_author' DraftAuthorModule, DraftRegulationChangeModule, DraftRegulationCancelModule, - NationalRegistryClientModule, + NationalRegistryV3ClientModule, ], providers: [DraftRegulationService, RegulationsService], controllers: [DraftRegulationController], diff --git a/apps/services/regulations-admin-backend/src/app/modules/draft_regulation/draft_regulation.service.ts b/apps/services/regulations-admin-backend/src/app/modules/draft_regulation/draft_regulation.service.ts index a92b649e388f..4de5b6eacefd 100644 --- a/apps/services/regulations-admin-backend/src/app/modules/draft_regulation/draft_regulation.service.ts +++ b/apps/services/regulations-admin-backend/src/app/modules/draft_regulation/draft_regulation.service.ts @@ -30,7 +30,7 @@ import { } from '@island.is/regulations/admin' import { Kennitala, RegQueryName } from '@island.is/regulations' import * as kennitala from 'kennitala' -import { NationalRegistryClientService } from '@island.is/clients/national-registry-v2' +import { NationalRegistryV3ClientService } from '@island.is/clients/national-registry-v3' import type { User } from '@island.is/auth-nest-tools' const sortImpacts = ( @@ -54,7 +54,7 @@ export class DraftRegulationService { private readonly regulationsService: RegulationsService, @Inject(LOGGER_PROVIDER) private readonly logger: Logger, - private readonly nationalRegistryApi: NationalRegistryClientService, + private readonly nationalRegistryApi: NationalRegistryV3ClientService, ) {} async getAll(user?: User, page = 1): Promise { @@ -337,12 +337,14 @@ export class DraftRegulationService { if (!author) { try { - const person = await this.nationalRegistryApi.getIndividual( + const person = await this.nationalRegistryApi.getAllDataIndividual( nationalId, + false, ) - if (person?.name) { + + if (person?.nafn) { author = { - name: person.name, + name: person.nafn, authorId: nationalId, } await this.draftAuthorService.create(author) diff --git a/charts/islandis/values.dev.yaml b/charts/islandis/values.dev.yaml index 037e9e7201fe..c8fbd907f529 100644 --- a/charts/islandis/values.dev.yaml +++ b/charts/islandis/values.dev.yaml @@ -1865,15 +1865,15 @@ regulations-admin-backend: IDENTITY_SERVER_CLIENT_ID: '@island.is/clients/regulations-admin-api' IDENTITY_SERVER_ISSUER_URL: 'https://identity-server.dev01.devland.is' LOG_LEVEL: 'info' + NATIONAL_REGISTRY_B2C_CLIENT_ID: 'b464afdd-056b-406d-b650-6d41733cfeb7' + NATIONAL_REGISTRY_B2C_ENDPOINT: 'https://skraidentitydev.b2clogin.com/skraidentitydev.onmicrosoft.com/b2c_1_midlun_flow/oauth2/v2.0/token' + NATIONAL_REGISTRY_B2C_PATH: 'IS-DEV/GOV/10001/SKRA-Cloud-Protected/Midlun-v1' + NATIONAL_REGISTRY_B2C_SCOPE: 'https://skraidentitydev.onmicrosoft.com/midlun/.default' NODE_OPTIONS: '--max-old-space-size=460 -r dd-trace/init' SERVERSIDE_FEATURES_ON: '' XROAD_BASE_PATH: 'http://securityserver.dev01.devland.is' XROAD_BASE_PATH_WITH_ENV: 'http://securityserver.dev01.devland.is/r1/IS-DEV' XROAD_CLIENT_ID: 'IS-DEV/GOV/10000/island-is-client' - XROAD_NATIONAL_REGISTRY_REDIS_NODES: '["clustercfg.general-redis-cluster-group.5fzau3.euw1.cache.amazonaws.com:6379"]' - XROAD_NATIONAL_REGISTRY_SERVICE_PATH: 'IS-DEV/GOV/10001/SKRA-Protected/Einstaklingar-v1' - XROAD_TJODSKRA_API_PATH: '/SKRA-Protected/Einstaklingar-v1' - XROAD_TJODSKRA_MEMBER_CODE: '10001' XROAD_TLS_BASE_PATH: 'https://securityserver.dev01.devland.is' XROAD_TLS_BASE_PATH_WITH_ENV: 'https://securityserver.dev01.devland.is/r1/IS-DEV' grantNamespaces: @@ -1941,6 +1941,7 @@ regulations-admin-backend: CONFIGCAT_SDK_KEY: '/k8s/configcat/CONFIGCAT_SDK_KEY' DB_PASS: '/k8s/regulations-admin-backend/DB_PASSWORD' IDENTITY_SERVER_CLIENT_SECRET: '/k8s/services-regulations-admin/IDENTITY_SERVER_CLIENT_SECRET' + NATIONAL_REGISTRY_B2C_CLIENT_SECRET: '/k8s/api/NATIONAL_REGISTRY_B2C_CLIENT_SECRET' REGULATIONS_API_URL: '/k8s/api/REGULATIONS_API_URL' REGULATIONS_FILE_UPLOAD_KEY_DRAFT: '/k8s/api/REGULATIONS_FILE_UPLOAD_KEY_DRAFT' REGULATIONS_FILE_UPLOAD_KEY_PRESIGNED: '/k8s/api/REGULATIONS_FILE_UPLOAD_KEY_PRESIGNED' diff --git a/charts/islandis/values.prod.yaml b/charts/islandis/values.prod.yaml index a8633b01264a..92b9250b3f09 100644 --- a/charts/islandis/values.prod.yaml +++ b/charts/islandis/values.prod.yaml @@ -1733,15 +1733,15 @@ regulations-admin-backend: IDENTITY_SERVER_CLIENT_ID: '@island.is/clients/regulations-admin-api' IDENTITY_SERVER_ISSUER_URL: 'https://innskra.island.is' LOG_LEVEL: 'info' + NATIONAL_REGISTRY_B2C_CLIENT_ID: '2304d7ca-7ed3-4188-8b6d-e1b7e0e3df7f' + NATIONAL_REGISTRY_B2C_ENDPOINT: 'https://skraidentity.b2clogin.com/skraidentity.onmicrosoft.com/b2c_1_midlun_flow/oauth2/v2.0/token' + NATIONAL_REGISTRY_B2C_PATH: 'IS/GOV/6503760649/SKRA-Cloud-Protected/Midlun-v1' + NATIONAL_REGISTRY_B2C_SCOPE: 'https://skraidentity.onmicrosoft.com/midlun/.default' NODE_OPTIONS: '--max-old-space-size=460 -r dd-trace/init' SERVERSIDE_FEATURES_ON: 'driving-license-use-v1-endpoint-for-v2-comms' XROAD_BASE_PATH: 'http://securityserver.island.is' XROAD_BASE_PATH_WITH_ENV: 'http://securityserver.island.is/r1/IS' XROAD_CLIENT_ID: 'IS/GOV/5501692829/island-is-client' - XROAD_NATIONAL_REGISTRY_REDIS_NODES: '["clustercfg.general-redis-cluster-group.whakos.euw1.cache.amazonaws.com:6379"]' - XROAD_NATIONAL_REGISTRY_SERVICE_PATH: 'IS/GOV/6503760649/SKRA-Protected/Einstaklingar-v1' - XROAD_TJODSKRA_API_PATH: '/SKRA-Protected/Einstaklingar-v1' - XROAD_TJODSKRA_MEMBER_CODE: '6503760649' XROAD_TLS_BASE_PATH: 'https://securityserver.island.is' XROAD_TLS_BASE_PATH_WITH_ENV: 'https://securityserver.island.is/r1/IS' grantNamespaces: @@ -1809,6 +1809,7 @@ regulations-admin-backend: CONFIGCAT_SDK_KEY: '/k8s/configcat/CONFIGCAT_SDK_KEY' DB_PASS: '/k8s/regulations-admin-backend/DB_PASSWORD' IDENTITY_SERVER_CLIENT_SECRET: '/k8s/services-regulations-admin/IDENTITY_SERVER_CLIENT_SECRET' + NATIONAL_REGISTRY_B2C_CLIENT_SECRET: '/k8s/api/NATIONAL_REGISTRY_B2C_CLIENT_SECRET' REGULATIONS_API_URL: '/k8s/api/REGULATIONS_API_URL' REGULATIONS_FILE_UPLOAD_KEY_DRAFT: '/k8s/api/REGULATIONS_FILE_UPLOAD_KEY_DRAFT' REGULATIONS_FILE_UPLOAD_KEY_PRESIGNED: '/k8s/api/REGULATIONS_FILE_UPLOAD_KEY_PRESIGNED' diff --git a/charts/islandis/values.staging.yaml b/charts/islandis/values.staging.yaml index db15aaebed05..5fbb15a89a49 100644 --- a/charts/islandis/values.staging.yaml +++ b/charts/islandis/values.staging.yaml @@ -1607,15 +1607,15 @@ regulations-admin-backend: IDENTITY_SERVER_CLIENT_ID: '@island.is/clients/regulations-admin-api' IDENTITY_SERVER_ISSUER_URL: 'https://identity-server.staging01.devland.is' LOG_LEVEL: 'info' + NATIONAL_REGISTRY_B2C_CLIENT_ID: 'ca128c23-b43c-443d-bade-ec5a146a933f' + NATIONAL_REGISTRY_B2C_ENDPOINT: 'https://skraidentitydev.b2clogin.com/skraidentitystaging.onmicrosoft.com/b2c_1_midlun_flow/oauth2/v2.0/token' + NATIONAL_REGISTRY_B2C_PATH: 'IS-TEST/GOV/6503760649/SKRA-Cloud-Protected/Midlun-v1' + NATIONAL_REGISTRY_B2C_SCOPE: 'https://skraidentitystaging.onmicrosoft.com/midlun/.default' NODE_OPTIONS: '--max-old-space-size=460 -r dd-trace/init' SERVERSIDE_FEATURES_ON: '' XROAD_BASE_PATH: 'http://securityserver.staging01.devland.is' XROAD_BASE_PATH_WITH_ENV: 'http://securityserver.staging01.devland.is/r1/IS-TEST' XROAD_CLIENT_ID: 'IS-TEST/GOV/5501692829/island-is-client' - XROAD_NATIONAL_REGISTRY_REDIS_NODES: '["clustercfg.general-redis-cluster-group.ab9ckb.euw1.cache.amazonaws.com:6379"]' - XROAD_NATIONAL_REGISTRY_SERVICE_PATH: 'IS-TEST/GOV/6503760649/SKRA-Protected/Einstaklingar-v1' - XROAD_TJODSKRA_API_PATH: '/SKRA-Protected/Einstaklingar-v1' - XROAD_TJODSKRA_MEMBER_CODE: '6503760649' XROAD_TLS_BASE_PATH: 'https://securityserver.staging01.devland.is' XROAD_TLS_BASE_PATH_WITH_ENV: 'https://securityserver.staging01.devland.is/r1/IS-TEST' grantNamespaces: @@ -1683,6 +1683,7 @@ regulations-admin-backend: CONFIGCAT_SDK_KEY: '/k8s/configcat/CONFIGCAT_SDK_KEY' DB_PASS: '/k8s/regulations-admin-backend/DB_PASSWORD' IDENTITY_SERVER_CLIENT_SECRET: '/k8s/services-regulations-admin/IDENTITY_SERVER_CLIENT_SECRET' + NATIONAL_REGISTRY_B2C_CLIENT_SECRET: '/k8s/api/NATIONAL_REGISTRY_B2C_CLIENT_SECRET' REGULATIONS_API_URL: '/k8s/api/REGULATIONS_API_URL' REGULATIONS_FILE_UPLOAD_KEY_DRAFT: '/k8s/api/REGULATIONS_FILE_UPLOAD_KEY_DRAFT' REGULATIONS_FILE_UPLOAD_KEY_PRESIGNED: '/k8s/api/REGULATIONS_FILE_UPLOAD_KEY_PRESIGNED' From 94f993e3d9c46ccef0def45fb6cb51c870f898dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dvar=20Oddsson?= Date: Fri, 13 Sep 2024 17:05:30 +0000 Subject: [PATCH 05/27] chore(j-s): Left align table values (#16002) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../web/src/components/Table/CaseFileTable/CaseFileTable.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/judicial-system/web/src/components/Table/CaseFileTable/CaseFileTable.tsx b/apps/judicial-system/web/src/components/Table/CaseFileTable/CaseFileTable.tsx index 50912848c34a..e8ed7b1431e9 100644 --- a/apps/judicial-system/web/src/components/Table/CaseFileTable/CaseFileTable.tsx +++ b/apps/judicial-system/web/src/components/Table/CaseFileTable/CaseFileTable.tsx @@ -86,10 +86,10 @@ const CaseFileTable: FC = ({ - + {formatDate(file.created, "dd.MM.yyyy 'kl.' HH:mm")} - + {formatMessage(strings.submittedBy, { category: file.category, initials: getInitials(file.submittedBy), From ad1b7cfaeb90e01854abad128602d6ffee4f9060 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAnar=20Vestmann?= <43557895+RunarVestmann@users.noreply.github.com> Date: Sat, 14 Sep 2024 08:37:19 +0000 Subject: [PATCH 06/27] feat(contentful-apps): Team member filter tags field (#16006) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../fields/team-member-filter-tags-field.tsx | 193 ++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 apps/contentful-apps/pages/fields/team-member-filter-tags-field.tsx diff --git a/apps/contentful-apps/pages/fields/team-member-filter-tags-field.tsx b/apps/contentful-apps/pages/fields/team-member-filter-tags-field.tsx new file mode 100644 index 000000000000..c106f88e022e --- /dev/null +++ b/apps/contentful-apps/pages/fields/team-member-filter-tags-field.tsx @@ -0,0 +1,193 @@ +import { useEffect, useState } from 'react' +import { useDebounce } from 'react-use' +import { + CollectionProp, + EntryProps, + KeyValueMap, + QueryOptions, + SysLink, +} from 'contentful-management' +import type { CMAClient, FieldExtensionSDK } from '@contentful/app-sdk' +import { Checkbox, Spinner, Stack, Text } from '@contentful/f36-components' +import { useCMA, useSDK } from '@contentful/react-apps-toolkit' + +import { sortAlpha } from '@island.is/shared/utils' + +const DEBOUNCE_TIME = 500 + +const fetchAll = async (cma: CMAClient, query: QueryOptions) => { + let response: CollectionProp> | null = null + const items: EntryProps[] = [] + let limit = 100 + + while ((response === null || items.length < response.total) && limit > 0) { + try { + response = await cma.entry.getMany({ + query: { + ...query, + limit, + skip: items.length, + }, + }) + items.push(...response.items) + } catch (error) { + const isResponseTooBig = (error?.message as string) + ?.toLowerCase() + ?.includes('response size too big') + + if (isResponseTooBig) limit = Math.floor(limit / 2) + else throw error + } + } + + return items +} + +const TeamMemberFilterTagsField = () => { + const sdk = useSDK() + const cma = useCMA() + const [isLoading, setIsLoading] = useState(true) + + const [filterTagSysLinks, setFilterTagSysLinks] = useState( + sdk.field.getValue() ?? [], + ) + + const [tagGroups, setTagGroups] = useState< + { + tagGroup: EntryProps + tags: EntryProps[] + }[] + >([]) + + useEffect(() => { + sdk.window.startAutoResizer() + return () => { + sdk.window.stopAutoResizer() + } + }, [sdk.window]) + + useEffect(() => { + const fetchTeamList = async () => { + try { + const teamListResponse = await cma.entry.getMany({ + query: { + links_to_entry: sdk.entry.getSys().id, + content_type: 'teamList', + }, + }) + + if (teamListResponse.items.length === 0) { + setIsLoading(false) + return + } + + const tagGroupSysLinks: SysLink[] = + teamListResponse.items[0].fields.filterGroups?.[ + sdk.locales.default + ] ?? [] + + const promises = tagGroupSysLinks.map(async (tagGroupSysLink) => { + const [tagGroup, tags] = await Promise.all([ + cma.entry.get({ + entryId: tagGroupSysLink.sys.id, + }), + fetchAll(cma, { + links_to_entry: tagGroupSysLink.sys.id, + content_type: 'genericTag', + }), + ]) + + tags.sort((a, b) => { + return sortAlpha(sdk.locales.default)( + a.fields.title, + b.fields.title, + ) + }) + + return { tagGroup, tags } + }) + + setTagGroups(await Promise.all(promises)) + } finally { + setIsLoading(false) + } + } + + fetchTeamList() + }, [cma, sdk.entry, sdk.locales.default, setTagGroups]) + + useDebounce( + () => { + sdk.field.setValue(filterTagSysLinks) + }, + DEBOUNCE_TIME, + [filterTagSysLinks], + ) + + return ( + <> + {isLoading && } + + {!isLoading && ( + + {tagGroups.map(({ tagGroup, tags }) => { + return ( + + + {tagGroup.fields.title[sdk.locales.default]} + + + {tags.map((tag) => { + const isChecked = filterTagSysLinks.some( + (filterTagSysLink) => + filterTagSysLink.sys.id === tag.sys.id, + ) + return ( + { + setFilterTagSysLinks((prev) => { + const alreadyExists = prev.some( + (filterTagSysLink) => + filterTagSysLink.sys.id === tag.sys.id, + ) + if (alreadyExists) { + return prev.filter( + (filterTagSysLink) => + filterTagSysLink.sys.id !== tag.sys.id, + ) + } + return prev.concat({ + sys: { + id: tag.sys.id, + type: 'Link', + linkType: 'Entry', + }, + }) + }) + }} + > + {tag.fields.title[sdk.locales.default]} + + ) + })} + + + ) + })} + + )} + + ) +} + +export default TeamMemberFilterTagsField From 150ab3a4280ef3a7c01f68d8d18e17981c2d695e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAnar=20Vestmann?= <43557895+RunarVestmann@users.noreply.github.com> Date: Sat, 14 Sep 2024 08:43:42 +0000 Subject: [PATCH 07/27] feat(contentful-apps): Generic Tag Group - Display in cms what tags belong to group (#16008) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../GenericListEditor/GenericListEditor.tsx | 1 + .../fields/generic-tag-group-items-field.tsx | 180 ++++++++++++++++++ 2 files changed, 181 insertions(+) create mode 100644 apps/contentful-apps/pages/fields/generic-tag-group-items-field.tsx diff --git a/apps/contentful-apps/components/lists/GenericListEditor/GenericListEditor.tsx b/apps/contentful-apps/components/lists/GenericListEditor/GenericListEditor.tsx index d4c7ba5028f3..ec933a66f49d 100644 --- a/apps/contentful-apps/components/lists/GenericListEditor/GenericListEditor.tsx +++ b/apps/contentful-apps/components/lists/GenericListEditor/GenericListEditor.tsx @@ -84,6 +84,7 @@ export const GenericListEditor = () => { skip, 'fields.internalTitle[match]': searchValue, 'fields.genericList.sys.id': sdk.entry.getSys().id, + 'sys.archivedAt[exists]': false, }, }) if ( diff --git a/apps/contentful-apps/pages/fields/generic-tag-group-items-field.tsx b/apps/contentful-apps/pages/fields/generic-tag-group-items-field.tsx new file mode 100644 index 000000000000..6a577e389b9d --- /dev/null +++ b/apps/contentful-apps/pages/fields/generic-tag-group-items-field.tsx @@ -0,0 +1,180 @@ +import { useEffect, useRef, useState } from 'react' +import { useDebounce } from 'react-use' +import type { FieldExtensionSDK } from '@contentful/app-sdk' +import { + Box, + Button, + EntryCard, + Pagination, + Spinner, + Stack, + Text, + TextInput, +} from '@contentful/f36-components' +import { PlusIcon } from '@contentful/f36-icons' +import { useCMA, useSDK } from '@contentful/react-apps-toolkit' + +const LIST_ITEMS_PER_PAGE = 4 +const SEARCH_DEBOUNCE_TIME_IN_MS = 300 + +const GenericTagGroupItemsField = () => { + const [page, setPage] = useState(0) + const pageRef = useRef(0) + const [searchValue, setSearchValue] = useState('') + const searchValueRef = useRef('') + const [listItemResponse, setListItemResponse] = useState(null) + const [isLoading, setIsLoading] = useState(false) + + const [counter, setCounter] = useState(0) + + const sdk = useSDK() + const cma = useCMA() + + const skip = LIST_ITEMS_PER_PAGE * page + + const createGenericTag = async () => { + const tag = await cma.entry.create( + { + contentTypeId: 'genericTag', + environmentId: sdk.ids.environment, + spaceId: sdk.ids.space, + }, + { + fields: { + genericTagGroup: { + [sdk.locales.default]: { + sys: { + id: sdk.entry.getSys().id, + linkType: 'Entry', + type: 'Link', + }, + }, + }, + }, + }, + ) + sdk.navigator + .openEntry(tag.sys.id, { + slideIn: { waitForClose: true }, + }) + .then(() => { + setCounter((c) => c + 1) + }) + } + + useDebounce( + async () => { + setIsLoading(true) + try { + const response = await cma.entry.getMany({ + query: { + content_type: 'genericTag', + limit: LIST_ITEMS_PER_PAGE, + skip, + 'fields.internalTitle[match]': searchValue, + 'fields.genericTagGroup.sys.id': sdk.entry.getSys().id, + 'sys.archivedAt[exists]': false, + }, + }) + + if ( + searchValueRef.current === searchValue && + pageRef.current === page + ) { + setListItemResponse(response) + } + } finally { + setIsLoading(false) + } + }, + SEARCH_DEBOUNCE_TIME_IN_MS, + [page, searchValue, counter], + ) + + useEffect(() => { + sdk.window.startAutoResizer() + return () => { + sdk.window.stopAutoResizer() + } + }, [sdk.window]) + + return ( +
+ + + + + + + { + searchValueRef.current = ev.target.value + setSearchValue(ev.target.value) + setPage(0) + pageRef.current = 0 + }} + /> + + + + + + {listItemResponse?.items?.length > 0 && ( + <> + + + {listItemResponse.items.map((item) => ( + { + sdk.navigator + .openEntry(item.sys.id, { + slideIn: { waitForClose: true }, + }) + .then(() => { + setCounter((c) => c + 1) + }) + }} + /> + ))} + + + { + pageRef.current = newPage + setPage(newPage) + }} + /> + + )} + + {listItemResponse?.items?.length === 0 && ( + + No item was found + + )} + +
+ ) +} + +export default GenericTagGroupItemsField From eb982d4f493b8b5e355227a504fc511c9386998c Mon Sep 17 00:00:00 2001 From: mannipje <135017126+mannipje@users.noreply.github.com> Date: Sat, 14 Sep 2024 19:03:41 +0000 Subject: [PATCH 08/27] feat(eb): Add default header for sjukratryggingar organization (#16012) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../Wrapper/OrganizationWrapper.tsx | 9 ++- .../SjukratryggingarDefaultHeader.css.ts | 6 ++ .../SjukratryggingarDefaultHeader.tsx | 68 +++++++++++++++++++ .../Themes/SjukratryggingarTheme/index.ts | 2 + 4 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 apps/web/components/Organization/Wrapper/Themes/SjukratryggingarTheme/SjukratryggingarDefaultHeader.css.ts create mode 100644 apps/web/components/Organization/Wrapper/Themes/SjukratryggingarTheme/SjukratryggingarDefaultHeader.tsx diff --git a/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx b/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx index 8ab52c6f7263..4b5949d7021b 100644 --- a/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx +++ b/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx @@ -97,6 +97,7 @@ import { RikissaksoknariHeader } from './Themes/RikissaksoknariTheme' import { SAkFooter, SAkHeader } from './Themes/SAkTheme' import { ShhFooter, ShhHeader } from './Themes/SHHTheme' import { + SjukratryggingarDefaultHeader, SjukratryggingarFooter, SjukratryggingarHeader, } from './Themes/SjukratryggingarTheme' @@ -284,7 +285,13 @@ export const OrganizationHeader: React.FC< /> ) case 'sjukratryggingar': - return ( + return n('usingDefaultHeader', false) ? ( + + ) : ( +> = ({ organizationPage, logoAltText, isSubpage }) => { + const { linkResolver } = useLinkResolver() + + const { width } = useWindowSize() + + const themeProp = organizationPage.themeProperties + + return ( +
theme.breakpoints.lg && !isSubpage + ? themeProp.backgroundColor + : `linear-gradient(184.95deg, #40c5e5 8.38%, rgba(64, 197, 227, 0.1) 39.64%, rgba(244, 247, 247, 0) 49.64%), + linear-gradient(273.41deg, #f4f7f7 -9.24%, #40c5e5 66.78%, #a4def1 105.51%)`) ?? + '', + }} + className={styles.gridContainerWidth} + > + +
+ ) +} + +export default SjukratryggingarDefaultHeader diff --git a/apps/web/components/Organization/Wrapper/Themes/SjukratryggingarTheme/index.ts b/apps/web/components/Organization/Wrapper/Themes/SjukratryggingarTheme/index.ts index ac06b38cba6c..5fde0b8abe2a 100644 --- a/apps/web/components/Organization/Wrapper/Themes/SjukratryggingarTheme/index.ts +++ b/apps/web/components/Organization/Wrapper/Themes/SjukratryggingarTheme/index.ts @@ -1,7 +1,9 @@ import dynamic from 'next/dynamic' +import DefaultHeader from './SjukratryggingarDefaultHeader' import Header from './SjukratryggingarHeader' +export const SjukratryggingarDefaultHeader = DefaultHeader export const SjukratryggingarHeader = Header export const SjukratryggingarFooter = dynamic( From a88bbcfea6f24f922b23f3fbc11a6aa806aaffb1 Mon Sep 17 00:00:00 2001 From: mannipje <135017126+mannipje@users.noreply.github.com> Date: Mon, 16 Sep 2024 09:53:11 +0000 Subject: [PATCH 09/27] feat(web): Add default header for sak organization (#16015) --- .../Wrapper/OrganizationWrapper.css.ts | 14 ++++++++++++++ .../Organization/Wrapper/OrganizationWrapper.tsx | 12 +++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/apps/web/components/Organization/Wrapper/OrganizationWrapper.css.ts b/apps/web/components/Organization/Wrapper/OrganizationWrapper.css.ts index 74db88d4d267..9c260ce38f54 100644 --- a/apps/web/components/Organization/Wrapper/OrganizationWrapper.css.ts +++ b/apps/web/components/Organization/Wrapper/OrganizationWrapper.css.ts @@ -1,5 +1,7 @@ import { style } from '@vanilla-extract/css' +import { themeUtils } from '@island.is/island-ui/theme' + export const menuStyle = style({ position: 'relative', zIndex: 20, @@ -19,3 +21,15 @@ export const digitalIcelandHeaderTitle = style({ ['-webkit-text-fill-color' as any]: 'transparent', textShadow: '0px 0px #00000000', }) + +export const sakHeaderGridContainer = style({ + display: 'grid', + maxWidth: '1342px', + margin: '0 auto', + ...themeUtils.responsiveStyle({ + lg: { + gridTemplateRows: '315px', + gridTemplateColumns: '52fr 48fr', + }, + }), +}) diff --git a/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx b/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx index 4b5949d7021b..7505cc6e9795 100644 --- a/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx +++ b/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx @@ -374,7 +374,17 @@ export const OrganizationHeader: React.FC< /> ) case 'sak': - return ( + return n('usingDefaultHeader', false) ? ( + + ) : ( Date: Mon, 16 Sep 2024 10:33:27 +0000 Subject: [PATCH 10/27] feat(native-app): Implement universal links (#15961) - Only runs when the user is logged in and has passed the pin screen. - Improves notification handling to support passkey browser. Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../android/app/src/prod/AndroidManifest.xml | 12 +++ .../app/ios/IslandApp/IslandApp.entitlements | 1 + apps/native/app/package.json | 1 + .../app/src/hooks/use-deep-link-handling.ts | 73 ++++++++++++++ apps/native/app/src/index.tsx | 4 - apps/native/app/src/lib/deep-linking.ts | 11 ++- apps/native/app/src/screens/home/home.tsx | 37 ++++---- .../screens/notifications/notifications.tsx | 24 +++-- .../utils/lifecycle/setup-event-handlers.ts | 16 ---- .../utils/lifecycle/setup-notifications.ts | 95 ------------------- yarn.lock | 29 ++++-- 11 files changed, 147 insertions(+), 156 deletions(-) create mode 100644 apps/native/app/src/hooks/use-deep-link-handling.ts delete mode 100644 apps/native/app/src/utils/lifecycle/setup-notifications.ts diff --git a/apps/native/app/android/app/src/prod/AndroidManifest.xml b/apps/native/app/android/app/src/prod/AndroidManifest.xml index 7fb79e9924d4..afbc625e51d5 100644 --- a/apps/native/app/android/app/src/prod/AndroidManifest.xml +++ b/apps/native/app/android/app/src/prod/AndroidManifest.xml @@ -26,6 +26,18 @@ + + + + + + + + + + + + diff --git a/apps/native/app/ios/IslandApp/IslandApp.entitlements b/apps/native/app/ios/IslandApp/IslandApp.entitlements index 710049617b26..2cc8fb471c65 100644 --- a/apps/native/app/ios/IslandApp/IslandApp.entitlements +++ b/apps/native/app/ios/IslandApp/IslandApp.entitlements @@ -7,6 +7,7 @@ com.apple.developer.associated-domains webcredentials:island.is + applinks:island.is keychain-access-groups diff --git a/apps/native/app/package.json b/apps/native/app/package.json index b1604aa07f38..fdcf1cede4ab 100644 --- a/apps/native/app/package.json +++ b/apps/native/app/package.json @@ -57,6 +57,7 @@ "expo": "51.0.25", "expo-file-system": "17.0.1", "expo-haptics": "13.0.1", + "expo-linking": "6.3.1", "expo-local-authentication": "14.0.1", "expo-notifications": "0.28.9", "intl": "1.2.5", diff --git a/apps/native/app/src/hooks/use-deep-link-handling.ts b/apps/native/app/src/hooks/use-deep-link-handling.ts new file mode 100644 index 000000000000..aab6a9dbe2a5 --- /dev/null +++ b/apps/native/app/src/hooks/use-deep-link-handling.ts @@ -0,0 +1,73 @@ +import messaging, { + FirebaseMessagingTypes, +} from '@react-native-firebase/messaging' +import { useCallback, useEffect, useRef, useState } from 'react' +import { useURL } from 'expo-linking' +import { useMarkUserNotificationAsReadMutation } from '../graphql/types/schema' + +import { navigateToUniversalLink } from '../lib/deep-linking' +import { useBrowser } from '../lib/use-browser' +import { useAuthStore } from '../stores/auth-store' + +// Expo-style notification hook wrapping firebase. +function useLastNotificationResponse() { + const [lastNotificationResponse, setLastNotificationResponse] = + useState(null) + + useEffect(() => { + messaging() + .getInitialNotification() + .then((remoteMessage) => { + if (remoteMessage) { + setLastNotificationResponse(remoteMessage) + } + }) + + // Return the unsubscribe function as a useEffect destructor. + return messaging().onNotificationOpenedApp((remoteMessage) => { + setLastNotificationResponse(remoteMessage) + }) + }, []) + + return lastNotificationResponse +} + +export function useDeepLinkHandling() { + const url = useURL() + const notification = useLastNotificationResponse() + const [markUserNotificationAsRead] = useMarkUserNotificationAsReadMutation() + const lockScreenActivatedAt = useAuthStore( + ({ lockScreenActivatedAt }) => lockScreenActivatedAt, + ) + + const lastUrl = useRef(null) + const { openBrowser } = useBrowser() + + const handleUrl = useCallback( + (url?: string | null) => { + if (!url || lastUrl.current === url || lockScreenActivatedAt) { + return false + } + lastUrl.current = url + + navigateToUniversalLink({ link: url, openBrowser }) + return true + }, + [openBrowser, lastUrl, lockScreenActivatedAt], + ) + + useEffect(() => { + handleUrl(url) + }, [url, handleUrl]) + + useEffect(() => { + const url = notification?.data?.clickActionUrl + const wasHandled = handleUrl(url) + if (wasHandled && notification?.data?.notificationId) { + // Mark notification as read and seen + void markUserNotificationAsRead({ + variables: { id: Number(notification.data.notificationId) }, + }) + } + }, [notification, handleUrl, markUserNotificationAsRead]) +} diff --git a/apps/native/app/src/index.tsx b/apps/native/app/src/index.tsx index e01b5ee33d96..586fedb0f9a9 100644 --- a/apps/native/app/src/index.tsx +++ b/apps/native/app/src/index.tsx @@ -8,7 +8,6 @@ import { registerAllComponents } from './utils/lifecycle/setup-components' import { setupDevMenu } from './utils/lifecycle/setup-dev-menu' import { setupEventHandlers } from './utils/lifecycle/setup-event-handlers' import { setupGlobals } from './utils/lifecycle/setup-globals' -import { setupNotifications } from './utils/lifecycle/setup-notifications' import { setupRoutes } from './utils/lifecycle/setup-routes' import { performanceMetricsAppLaunched } from './utils/performance-metrics' @@ -25,9 +24,6 @@ async function startApp() { // Setup app routing layer setupRoutes() - // Setup notifications - setupNotifications() - // Initialize Apollo client. This must be done before registering components await initializeApolloClient() diff --git a/apps/native/app/src/lib/deep-linking.ts b/apps/native/app/src/lib/deep-linking.ts index 9bb7670d3af0..1857145c1fd0 100644 --- a/apps/native/app/src/lib/deep-linking.ts +++ b/apps/native/app/src/lib/deep-linking.ts @@ -186,16 +186,18 @@ export function navigateTo(url: string, extraProps: any = {}) { } /** - * Navigate to a notification ClickActionUrl, if our mapping does not return a valid screen within the app - open a webview. + * Navigate to a specific universal link, if our mapping does not return a valid screen within the app - open a webview. */ -export function navigateToNotification({ +export function navigateToUniversalLink({ link, componentId, + openBrowser = openNativeBrowser, }: { // url to navigate to link?: NotificationMessage['link']['url'] // componentId to open web browser in componentId?: string + openBrowser?: (link: string, componentId?: string) => void }) { // If no link do nothing if (!link) return @@ -216,13 +218,14 @@ export function navigateToNotification({ }, }) } - // TODO: When navigating to a link from notification works, implement a way to use useBrowser.openBrowser here - openNativeBrowser(link, componentId ?? ComponentRegistry.HomeScreen) + + openBrowser(link, componentId ?? ComponentRegistry.HomeScreen) } // Map between notification link and app screen const urlMapping: { [key: string]: string } = { '/minarsidur/postholf/:id': '/inbox/:id', + '/minarsidur/postholf': '/inbox', '/minarsidur/min-gogn/stillingar': '/settings', '/minarsidur/skirteini': '/wallet', '/minarsidur/skirteini/tjodskra/vegabref/:id': '/walletpassport/:id', diff --git a/apps/native/app/src/screens/home/home.tsx b/apps/native/app/src/screens/home/home.tsx index d9d375c872b3..07106e266c41 100644 --- a/apps/native/app/src/screens/home/home.tsx +++ b/apps/native/app/src/screens/home/home.tsx @@ -20,12 +20,21 @@ import { BottomTabsIndicator } from '../../components/bottom-tabs-indicator/bott import { createNavigationOptionHooks } from '../../hooks/create-navigation-option-hooks' import { useAndroidNotificationPermission } from '../../hooks/use-android-notification-permission' import { useConnectivityIndicator } from '../../hooks/use-connectivity-indicator' +import { useDeepLinkHandling } from '../../hooks/use-deep-link-handling' import { useNotificationsStore } from '../../stores/notifications-store' +import { + preferencesStore, + usePreferencesStore, +} from '../../stores/preferences-store' import { useUiStore } from '../../stores/ui-store' import { isAndroid } from '../../utils/devices' import { getRightButtons } from '../../utils/get-main-root' -import { handleInitialNotification } from '../../utils/lifecycle/setup-notifications' import { testIDs } from '../../utils/test-ids' +import { + AirDiscountModule, + useGetAirDiscountQuery, + validateAirDiscountInitialData, +} from './air-discount-module' import { ApplicationsModule, useListApplicationsQuery, @@ -37,26 +46,17 @@ import { useListDocumentsQuery, validateInboxInitialData, } from './inbox-module' +import { + LicensesModule, + useGetLicensesData, + validateLicensesInitialData, +} from './licenses-module' import { OnboardingModule } from './onboarding-module' import { - VehiclesModule, useListVehiclesQuery, validateVehiclesInitialData, + VehiclesModule, } from './vehicles-module' -import { - preferencesStore, - usePreferencesStore, -} from '../../stores/preferences-store' -import { - AirDiscountModule, - useGetAirDiscountQuery, - validateAirDiscountInitialData, -} from './air-discount-module' -import { - LicensesModule, - validateLicensesInitialData, - useGetLicensesData, -} from './licenses-module' interface ListItem { id: string @@ -150,6 +150,8 @@ export const MainHomeScreen: NavigationFunctionComponent = ({ ({ widgetsInitialised }) => widgetsInitialised, ) + useDeepLinkHandling() + const applicationsRes = useListApplicationsQuery({ skip: !applicationsWidgetEnabled, }) @@ -258,9 +260,6 @@ export const MainHomeScreen: NavigationFunctionComponent = ({ checkUnseen() // Get user locale from server getAndSetLocale() - - // Handle initial notification - handleInitialNotification() }, []) const refetch = useCallback(async () => { diff --git a/apps/native/app/src/screens/notifications/notifications.tsx b/apps/native/app/src/screens/notifications/notifications.tsx index aa54c3fe624a..913c1b6d6d50 100644 --- a/apps/native/app/src/screens/notifications/notifications.tsx +++ b/apps/native/app/src/screens/notifications/notifications.tsx @@ -34,7 +34,7 @@ import { } from '../../graphql/types/schema' import { createNavigationOptionHooks } from '../../hooks/create-navigation-option-hooks' -import { navigateTo, navigateToNotification } from '../../lib/deep-linking' +import { navigateTo, navigateToUniversalLink } from '../../lib/deep-linking' import { useNotificationsStore } from '../../stores/notifications-store' import { createSkeletonArr, @@ -45,6 +45,7 @@ import { testIDs } from '../../utils/test-ids' import settings from '../../assets/icons/settings.png' import inboxRead from '../../assets/icons/inbox-read.png' import emptyIllustrationSrc from '../../assets/illustrations/le-company-s3.png' +import { useBrowser } from '../../lib/use-browser' const LoadingWrapper = styled.View` padding-vertical: ${({ theme }) => theme.spacing[3]}px; @@ -85,6 +86,7 @@ export const NotificationsScreen: NavigationFunctionComponent = ({ componentId, }) => { useNavigationOptions(componentId) + const { openBrowser } = useBrowser() const intl = useIntl() const theme = useTheme() const client = useApolloClient() @@ -147,15 +149,19 @@ export const NotificationsScreen: NavigationFunctionComponent = ({ return data?.userNotifications?.data || [] }, [data, loading]) - const onNotificationPress = useCallback((notification: Notification) => { - // Mark notification as read and seen - void markUserNotificationAsRead({ variables: { id: notification.id } }) + const onNotificationPress = useCallback( + (notification: Notification) => { + // Mark notification as read and seen + void markUserNotificationAsRead({ variables: { id: notification.id } }) - navigateToNotification({ - componentId, - link: notification.message?.link?.url, - }) - }, []) + navigateToUniversalLink({ + componentId, + link: notification.message?.link?.url, + openBrowser, + }) + }, + [markUserNotificationAsRead, componentId, openBrowser], + ) const handleEndReached = async () => { if ( diff --git a/apps/native/app/src/utils/lifecycle/setup-event-handlers.ts b/apps/native/app/src/utils/lifecycle/setup-event-handlers.ts index 4512b2028297..b5db7bf84337 100644 --- a/apps/native/app/src/utils/lifecycle/setup-event-handlers.ts +++ b/apps/native/app/src/utils/lifecycle/setup-event-handlers.ts @@ -28,13 +28,6 @@ let backgroundAppLockTimeout: ReturnType export function setupEventHandlers() { // Listen for url events through iOS and Android's Linking library Linking.addEventListener('url', ({ url }) => { - console.log('URL', url) - Linking.canOpenURL(url).then((supported) => { - if (supported) { - evaluateUrl(url) - } - }) - // Handle Cognito if (/cognito/.test(url)) { const [, hash] = url.split('#') @@ -66,15 +59,6 @@ export function setupEventHandlers() { }) } - // Get initial url and pass to the opener - Linking.getInitialURL() - .then((url) => { - if (url) { - Linking.openURL(url) - } - }) - .catch((err) => console.error('An error occurred in getInitialURL: ', err)) - Navigation.events().registerBottomTabSelectedListener((e) => { uiStore.setState({ unselectedTab: e.unselectedTabIndex, diff --git a/apps/native/app/src/utils/lifecycle/setup-notifications.ts b/apps/native/app/src/utils/lifecycle/setup-notifications.ts deleted file mode 100644 index 363fe1559da2..000000000000 --- a/apps/native/app/src/utils/lifecycle/setup-notifications.ts +++ /dev/null @@ -1,95 +0,0 @@ -import messaging, { - FirebaseMessagingTypes, -} from '@react-native-firebase/messaging' -import { - DEFAULT_ACTION_IDENTIFIER, - Notification, - NotificationResponse, -} from 'expo-notifications' -import { navigateTo, navigateToNotification } from '../../lib/deep-linking' - -export const ACTION_IDENTIFIER_NO_OPERATION = 'NOOP' - -export async function handleNotificationResponse({ - actionIdentifier, - notification, -}: NotificationResponse) { - const link = - notification.request.content.data?.clickActionUrl ?? - notification.request.content.data?.link - - if ( - typeof link === 'string' && - actionIdentifier !== ACTION_IDENTIFIER_NO_OPERATION - ) { - navigateToNotification({ link }) - } else { - navigateTo('/notifications') - } -} - -function mapRemoteMessage( - remoteMessage: FirebaseMessagingTypes.RemoteMessage, -): Notification { - return { - date: remoteMessage.sentTime ?? 0, - request: { - content: { - title: remoteMessage.notification?.title || null, - subtitle: null, - body: remoteMessage.notification?.body || null, - data: { - link: remoteMessage.notification?.android?.link, - ...remoteMessage.data, - }, - sound: 'default', - }, - identifier: remoteMessage.messageId ?? '', - trigger: { - type: 'push', - }, - }, - } -} - -export function setupNotifications() { - // FCMs - - messaging().onNotificationOpenedApp((remoteMessage) => - handleNotificationResponse({ - notification: mapRemoteMessage(remoteMessage), - actionIdentifier: DEFAULT_ACTION_IDENTIFIER, - }), - ) - - messaging().onMessage((remoteMessage) => - handleNotificationResponse({ - notification: mapRemoteMessage(remoteMessage), - actionIdentifier: ACTION_IDENTIFIER_NO_OPERATION, - }), - ) - - messaging().setBackgroundMessageHandler((remoteMessage) => - handleNotificationResponse({ - notification: mapRemoteMessage(remoteMessage), - actionIdentifier: ACTION_IDENTIFIER_NO_OPERATION, - }), - ) -} - -/** - * Handle initial notification when app is closed and opened from a notification - */ -export function handleInitialNotification() { - // FCMs - messaging() - .getInitialNotification() - .then((remoteMessage) => { - if (remoteMessage) { - void handleNotificationResponse({ - notification: mapRemoteMessage(remoteMessage), - actionIdentifier: DEFAULT_ACTION_IDENTIFIER, - }) - } - }) -} diff --git a/yarn.lock b/yarn.lock index 474666f89dde..911d3842134f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10753,8 +10753,8 @@ __metadata: linkType: hard "@expo/metro-config@npm:~0.18.6": - version: 0.18.7 - resolution: "@expo/metro-config@npm:0.18.7" + version: 0.18.11 + resolution: "@expo/metro-config@npm:0.18.11" dependencies: "@babel/core": ^7.20.0 "@babel/generator": ^7.20.5 @@ -10774,7 +10774,7 @@ __metadata: lightningcss: ~1.19.0 postcss: ~8.4.32 resolve-from: ^5.0.0 - checksum: f9212492ed5bb1d28bb506280055d7488f1d7d2013f65fdaaec8158de07cdd46c887d30ae206d65b89fee24bf1def20b28caf1563f54c3daabe02ad0d210ee3e + checksum: 4de79b97c6d818a487c6eaa83a55d3d9d1a1b28262507d74ad407fa22c2c32658d2cd2fa38babf82c32cf58239aff2c5d85e130609eaa34ed29a8e20a295cd7f languageName: node linkType: hard @@ -12544,6 +12544,7 @@ __metadata: expo: 51.0.25 expo-file-system: 17.0.1 expo-haptics: 13.0.1 + expo-linking: 6.3.1 expo-local-authentication: 14.0.1 expo-notifications: 0.28.9 intl: 1.2.5 @@ -24933,8 +24934,8 @@ __metadata: linkType: hard "babel-preset-expo@npm:~11.0.13": - version: 11.0.13 - resolution: "babel-preset-expo@npm:11.0.13" + version: 11.0.14 + resolution: "babel-preset-expo@npm:11.0.14" dependencies: "@babel/plugin-proposal-decorators": ^7.12.9 "@babel/plugin-transform-export-namespace-from": ^7.22.11 @@ -24946,7 +24947,7 @@ __metadata: babel-plugin-react-compiler: ^0.0.0-experimental-592953e-20240517 babel-plugin-react-native-web: ~0.19.10 react-refresh: ^0.14.2 - checksum: 6bfc721da903591bf94c73b711ead8ce5d28739fa6b5c893581c4c5f70f164aa6930982300066d412ce81e0c11e9e531e5c339751b05f002a37909e096f54b06 + checksum: b41c3fab6592fceb4ae020a0a79cb8e1d2e0354daca1d468e7db2c3033a17d654ac4627fb0b26f728809bc9810b7a1065dfd2a8a1f05fdbc83bacdc90e8e79dd languageName: node linkType: hard @@ -32265,13 +32266,13 @@ __metadata: linkType: hard "expo-font@npm:~12.0.9": - version: 12.0.9 - resolution: "expo-font@npm:12.0.9" + version: 12.0.10 + resolution: "expo-font@npm:12.0.10" dependencies: fontfaceobserver: ^2.1.0 peerDependencies: expo: "*" - checksum: adad225ed6002d5d527808b8f463bc59a1a1626fb2ff34918dcbd2172757977c056101f737ed9523f6d55e0aa88a64988002eb9b6d22f379d5956883f7451379 + checksum: c8fdc046158d4c2d71d81fcd9ba115bc0e142bc0d637ae9b5fea04cd816c62c051f63e44685530109106565d29feca2035ef6123c56cf9c951d0a2775a8cd9a7 languageName: node linkType: hard @@ -32293,6 +32294,16 @@ __metadata: languageName: node linkType: hard +"expo-linking@npm:6.3.1": + version: 6.3.1 + resolution: "expo-linking@npm:6.3.1" + dependencies: + expo-constants: ~16.0.0 + invariant: ^2.2.4 + checksum: 32e2dbcffc802fc6570a5a9cd7839c873f6cfc40730f1cf3cdabeb2782c30b54455d41c98708dbba2649941d5ff8cb591b85689f9c1a3b7a3fcb20011aae0cb5 + languageName: node + linkType: hard + "expo-local-authentication@npm:14.0.1": version: 14.0.1 resolution: "expo-local-authentication@npm:14.0.1" From 02e8a3c3863c09d7ad85574800c66d579648eb8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eir=C3=ADkur=20Hei=C3=B0ar=20Nilsson?= Date: Mon, 16 Sep 2024 14:34:25 +0000 Subject: [PATCH 11/27] feat(user-notifications): Send notificationId in push notifications (#15962) So the notification can be easily marked as read/seen when the user taps on the push notification. Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../notifications/notificationDispatch.service.ts | 11 ++++++++++- .../notificationsWorker.service.spec.ts | 13 +++++++++---- .../notificationsWorker.service.ts | 15 ++++++++++----- 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/apps/services/user-notification/src/app/modules/notifications/notificationDispatch.service.ts b/apps/services/user-notification/src/app/modules/notifications/notificationDispatch.service.ts index 728408e4361e..0b7d5cfb55c4 100644 --- a/apps/services/user-notification/src/app/modules/notifications/notificationDispatch.service.ts +++ b/apps/services/user-notification/src/app/modules/notifications/notificationDispatch.service.ts @@ -24,10 +24,12 @@ export class NotificationDispatchService { notification, nationalId, messageId, + notificationId, }: { notification: Notification nationalId: string messageId: string + notificationId?: number | null }): Promise { const tokens = await this.getDeviceTokens(nationalId, messageId) @@ -42,7 +44,12 @@ export class NotificationDispatchService { for (const token of tokens) { try { - await this.sendNotificationToToken(notification, token, messageId) + await this.sendNotificationToToken( + notification, + token, + messageId, + notificationId, + ) } catch (error) { await this.handleSendError(error, nationalId, token, messageId) } @@ -82,6 +89,7 @@ export class NotificationDispatchService { notification: Notification, token: string, messageId: string, + notificationId?: number | null, ): Promise { const message = { token, @@ -92,6 +100,7 @@ export class NotificationDispatchService { data: { messageId, clickActionUrl: notification.clickActionUrl, + ...(notificationId && { notificationId: String(notificationId) }), }, } diff --git a/apps/services/user-notification/src/app/modules/notifications/notificationsWorker/notificationsWorker.service.spec.ts b/apps/services/user-notification/src/app/modules/notifications/notificationsWorker/notificationsWorker.service.spec.ts index 49f10d6ca335..a34181153ae7 100644 --- a/apps/services/user-notification/src/app/modules/notifications/notificationsWorker/notificationsWorker.service.spec.ts +++ b/apps/services/user-notification/src/app/modules/notifications/notificationsWorker/notificationsWorker.service.spec.ts @@ -227,18 +227,23 @@ describe('NotificationsWorkerService', () => { expect(emailService.sendEmail).toHaveBeenCalledTimes(2) + // should write the messages to db + const messages = await notificationModel.findAll() + const recipientMessage = messages.find( + (message) => message.recipient === userWithDelegations.nationalId, + ) + expect(messages).toHaveLength(2) + expect(recipientMessage).toBeDefined() + // should only send push notification for primary recipient expect(notificationDispatch.sendPushNotification).toHaveBeenCalledTimes(1) expect(notificationDispatch.sendPushNotification).toHaveBeenCalledWith( expect.objectContaining({ nationalId: userWithDelegations.nationalId, + notificationId: recipientMessage.id, }), ) - // should write the messages to db - const messages = await notificationModel.findAll() - expect(messages).toHaveLength(2) - // should have gotten user profile for primary recipient expect( userProfileApi.userProfileControllerFindUserProfile, diff --git a/apps/services/user-notification/src/app/modules/notifications/notificationsWorker/notificationsWorker.service.ts b/apps/services/user-notification/src/app/modules/notifications/notificationsWorker/notificationsWorker.service.ts index 0406fa17612a..764593d87619 100644 --- a/apps/services/user-notification/src/app/modules/notifications/notificationsWorker/notificationsWorker.service.ts +++ b/apps/services/user-notification/src/app/modules/notifications/notificationsWorker/notificationsWorker.service.ts @@ -51,6 +51,7 @@ type HandleNotification = { emailNotifications: boolean locale?: string } + notificationId?: number | null messageId: string message: CreateHnippNotificationDto } @@ -93,6 +94,7 @@ export class NotificationsWorkerService implements OnApplicationBootstrap { async handleDocumentNotification({ profile, messageId, + notificationId, message, }: HandleNotification) { // don't send message unless user wants this type of notification and national id is a person. @@ -126,6 +128,7 @@ export class NotificationsWorkerService implements OnApplicationBootstrap { nationalId: profile.nationalId, notification, messageId, + notificationId, }) } @@ -342,12 +345,13 @@ export class NotificationsWorkerService implements OnApplicationBootstrap { await this.sleepOutsideWorkingHours(messageId) const notification = { messageId, ...message } - const messageIdExists = await this.notificationModel.count({ + let dbNotification = await this.notificationModel.findOne({ where: { messageId }, + attributes: ['id'], }) - if (messageIdExists > 0) { - // messageId exists do nothing + if (dbNotification) { + // messageId exists in db, do nothing this.logger.info('notification with messageId already exists in db', { messageId, }) @@ -355,8 +359,8 @@ export class NotificationsWorkerService implements OnApplicationBootstrap { // messageId does not exist // write to db try { - const res = await this.notificationModel.create(notification) - if (res) { + dbNotification = await this.notificationModel.create(notification) + if (dbNotification) { this.logger.info('notification written to db', { notification, messageId, @@ -398,6 +402,7 @@ export class NotificationsWorkerService implements OnApplicationBootstrap { const handleNotificationArgs: HandleNotification = { profile: { ...profile, nationalId: message.recipient }, messageId, + notificationId: dbNotification?.id, message, } From 97928c5597a3172d4a8c459dec8ad0058dbd8b66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9E=C3=B3r=C3=B0ur=20H?= Date: Mon, 16 Sep 2024 16:00:48 +0000 Subject: [PATCH 12/27] fix(service-portal): Add error toast to overveiw (#16021) * Add error toast to overveiw * Remove console log * Fix focus on checkmark inbox --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../components/DocumentLine/DocumentLineV2.tsx | 17 ++++++++++------- .../documents/src/hooks/useIsChildFocused.ts | 13 ++++++++++--- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/libs/service-portal/documents/src/components/DocumentLine/DocumentLineV2.tsx b/libs/service-portal/documents/src/components/DocumentLine/DocumentLineV2.tsx index 3249b6ba453c..31e4158d50f9 100644 --- a/libs/service-portal/documents/src/components/DocumentLine/DocumentLineV2.tsx +++ b/libs/service-portal/documents/src/components/DocumentLine/DocumentLineV2.tsx @@ -3,7 +3,7 @@ import format from 'date-fns/format' import { FC, useEffect, useRef, useState } from 'react' import { DocumentV2, DocumentV2Content } from '@island.is/api/schema' -import { Box, Text, LoadingDots, Icon } from '@island.is/island-ui/core' +import { Box, Text, LoadingDots, Icon, toast } from '@island.is/island-ui/core' import { dateFormat } from '@island.is/shared/constants' import { m } from '@island.is/service-portal/core' import * as styles from './DocumentLine.css' @@ -75,7 +75,7 @@ export const DocumentLine: FC = ({ const avatarRef = useRef(null) const isFocused = useIsChildFocusedorHovered(wrapperRef) - const isAvatarFocused = useIsChildFocusedorHovered(avatarRef) + const isAvatarFocused = useIsChildFocusedorHovered(avatarRef, false) useEffect(() => { setHasFocusOrHover(isFocused) @@ -135,11 +135,14 @@ export const DocumentLine: FC = ({ } }, onError: () => { - setDocumentDisplayError( - formatMessage(messages.documentFetchError, { - senderName: documentLine.sender?.name ?? '', - }), - ) + const errorMessage = formatMessage(messages.documentFetchError, { + senderName: documentLine.sender?.name ?? '', + }) + if (asFrame) { + toast.error(errorMessage, { toastId: 'overview-doc-error' }) + } else { + setDocumentDisplayError(errorMessage) + } }, }) diff --git a/libs/service-portal/documents/src/hooks/useIsChildFocused.ts b/libs/service-portal/documents/src/hooks/useIsChildFocused.ts index 205c90cc1171..6b09e75c21bc 100644 --- a/libs/service-portal/documents/src/hooks/useIsChildFocused.ts +++ b/libs/service-portal/documents/src/hooks/useIsChildFocused.ts @@ -1,6 +1,9 @@ import { RefObject, useEffect, useState } from 'react' -export const useIsChildFocusedorHovered = (ref: RefObject) => { +export const useIsChildFocusedorHovered = ( + ref: RefObject, + focus = true, +) => { const [isFocused, setIsFocused] = useState(false) const [isHovered, setIsHovered] = useState(false) @@ -21,11 +24,15 @@ export const useIsChildFocusedorHovered = (ref: RefObject) => { } } - document.addEventListener('focusin', handleFocus) + if (focus) { + document.addEventListener('focusin', handleFocus) + } document.addEventListener('mouseover', handleHover) return () => { - document.removeEventListener('focusin', handleFocus) + if (focus) { + document.removeEventListener('focusin', handleFocus) + } document.removeEventListener('mouseover', handleHover) } }, [ref]) From 294d0124edb5e55406e35a5b04f6c0a66f8ff0eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9E=C3=B3r=C3=B0ur=20H?= Date: Mon, 16 Sep 2024 16:38:50 +0000 Subject: [PATCH 13/27] fix(regulations-admin): Update styling. Update presigned query. (#16007) * Update styling. Update presigned query * little renaming --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../Regulations/RegulationTexts.types.ts | 2 +- .../lib/api-domains-regulations.resolver.ts | 15 ++++---- .../src/lib/dto/createPresignedPost.input.ts | 2 +- .../src/components/TaskList.tsx | 34 +++++++++++-------- .../src/components/impacts/ImpactListItem.tsx | 2 +- .../regulations-admin/src/utils/dataHooks.ts | 8 +++-- 6 files changed, 36 insertions(+), 27 deletions(-) diff --git a/apps/web/components/Regulations/RegulationTexts.types.ts b/apps/web/components/Regulations/RegulationTexts.types.ts index 9db3ce98e3da..323337272156 100644 --- a/apps/web/components/Regulations/RegulationTexts.types.ts +++ b/apps/web/components/Regulations/RegulationTexts.types.ts @@ -88,7 +88,7 @@ export type RegulationPageTexts = Partial< | 'historyTitle' // 'Breytingasaga reglugerðar ${name}' | 'historyStart' // 'Stofnreglugerð gefin út' | 'historyStartAmending' // 'Reglugerðin gefin út' - | 'historyChange' // 'Breytt af ${name}' + | 'historyChange' // 'Breytt með ${name}' | 'historyCancel' // 'Brottfelld af ${name}' | 'historyCurrentVersion' // 'Núgildandi útgáfa' | 'historyPastSplitter' // 'Gildandi breytingar' diff --git a/libs/api/domains/regulations/src/lib/api-domains-regulations.resolver.ts b/libs/api/domains/regulations/src/lib/api-domains-regulations.resolver.ts index af38f214031d..054eca834d73 100644 --- a/libs/api/domains/regulations/src/lib/api-domains-regulations.resolver.ts +++ b/libs/api/domains/regulations/src/lib/api-domains-regulations.resolver.ts @@ -1,12 +1,13 @@ import { Args, Mutation, Query, Resolver } from '@nestjs/graphql' import graphqlTypeJson from 'graphql-type-json' - +import { UseGuards } from '@nestjs/common' import { RegulationsService } from '@island.is/clients/regulations' +import { IdsUserGuard, ScopesGuard } from '@island.is/auth-nest-tools' import { RegulationSearchResults, RegulationYears, - RegulationListItem, } from '@island.is/regulations/web' +import { Audit } from '@island.is/nest/audit' import { Regulation, RegulationDiff, @@ -20,18 +21,20 @@ import { GetRegulationInput } from './dto/getRegulation.input' import { GetRegulationsLawChaptersInput } from './dto/getRegulationsLawChapters.input' import { GetRegulationsMinistriesInput } from './dto/getRegulationsMinistriesInput.input' import { GetRegulationsSearchInput } from './dto/getRegulationsSearch.input' -import { CreatePresignedPostInput } from './dto/createPresignedPost.input' +import { CreateRegulationPresignedPostInput } from './dto/createPresignedPost.input' import { PresignedPostResults } from '@island.is/regulations/admin' const validPage = (page: number | undefined) => (page && page >= 1 ? page : 1) - +@Audit({ namespace: '@island.is/api/regulations' }) @Resolver() export class RegulationsResolver { constructor(private regulationsService: RegulationsService) {} - @Mutation(() => graphqlTypeJson) + @Audit() + @UseGuards(IdsUserGuard, ScopesGuard) + @Mutation(() => graphqlTypeJson, { name: 'regulationCreatePresignedPost' }) createPresignedPost( - @Args('input') input: CreatePresignedPostInput, + @Args('input') input: CreateRegulationPresignedPostInput, ): Promise { return this.regulationsService.createPresignedPost( input.fileName, diff --git a/libs/api/domains/regulations/src/lib/dto/createPresignedPost.input.ts b/libs/api/domains/regulations/src/lib/dto/createPresignedPost.input.ts index aebda893850b..ff8b66208ea1 100644 --- a/libs/api/domains/regulations/src/lib/dto/createPresignedPost.input.ts +++ b/libs/api/domains/regulations/src/lib/dto/createPresignedPost.input.ts @@ -2,7 +2,7 @@ import { Field, InputType } from '@nestjs/graphql' import { IsString } from 'class-validator' @InputType() -export class CreatePresignedPostInput { +export class CreateRegulationPresignedPostInput { @Field() @IsString() readonly fileName!: string diff --git a/libs/portals/admin/regulations-admin/src/components/TaskList.tsx b/libs/portals/admin/regulations-admin/src/components/TaskList.tsx index c4b7f6e657ef..94d79382522b 100644 --- a/libs/portals/admin/regulations-admin/src/components/TaskList.tsx +++ b/libs/portals/admin/regulations-admin/src/components/TaskList.tsx @@ -30,7 +30,7 @@ export const TaskList = () => { if (loading) { return ( - + ) @@ -38,25 +38,29 @@ export const TaskList = () => { if (error) { return ( - - - + + + + + ) } if (drafts && drafts.length === 0) { return ( - - - + + + + + ) } diff --git a/libs/portals/admin/regulations-admin/src/components/impacts/ImpactListItem.tsx b/libs/portals/admin/regulations-admin/src/components/impacts/ImpactListItem.tsx index 59b7f0125f04..7f1179e4dbd7 100644 --- a/libs/portals/admin/regulations-admin/src/components/impacts/ImpactListItem.tsx +++ b/libs/portals/admin/regulations-admin/src/components/impacts/ImpactListItem.tsx @@ -72,7 +72,7 @@ export const ImpactListItem = (props: ImpactListItemProps) => { variant="small" color={getCurrentEffect(effect) ? 'mint800' : 'blueberry600'} > - Breytt af{' '} + Breytt með{' '} {getCurrentEffect(effect) ? 'núverandi reglugerð' : effect.name}
diff --git a/libs/portals/admin/regulations-admin/src/utils/dataHooks.ts b/libs/portals/admin/regulations-admin/src/utils/dataHooks.ts index cba4e71d4a79..ecd9499fec78 100644 --- a/libs/portals/admin/regulations-admin/src/utils/dataHooks.ts +++ b/libs/portals/admin/regulations-admin/src/utils/dataHooks.ts @@ -46,8 +46,10 @@ type QueryResult = // --------------------------------------------------------------------------- export const CreatePresignedPostMutation = gql` - mutation CreatePresignedPostMutation($input: CreatePresignedPostInput!) { - createPresignedPost(input: $input) + mutation CreatePresignedPostMutation( + $input: CreateRegulationPresignedPostInput! + ) { + regulationCreatePresignedPost(input: $input) } ` export type UploadingState = @@ -97,7 +99,7 @@ export const useS3Upload = () => { }, }, }) - return post.data?.createPresignedPost.data + return post.data?.regulationCreatePresignedPost.data } catch (error) { setUploadStatus({ uploading: false, From 253f34678ea027aef22e95384bf261172b1d21ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gunnlaugur=20Gu=C3=B0mundsson?= <34029342+GunnlaugurG@users.noreply.github.com> Date: Mon, 16 Sep 2024 17:03:08 +0000 Subject: [PATCH 14/27] feat(auth-admin): Delegation reference id (#15932) * created new admin module for paper delegations and get route * field resolver DelegationAdminModel * lookup for delegations with detailed view * Cleanup and rest and graphql for DeleteDelegation * small cleanup * chore: nx format:write update dirty files * move delegationAdmin service to admin-api from delegation-api * chore: nx format:write update dirty files * fix config value * chore: charts update dirty files * fix api build issues * fix pr comments * delegation reference id added * removed unwanted char * added comment for reference id * add unique constraint to reference_id in delegations table * simplifying the migration script * fix model with simplification --------- Co-authored-by: andes-it Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../20240910092516-delegation-reference-id.js | 19 +++++++++++++++++++ .../src/lib/delegations/dto/delegation.dto.ts | 5 +++++ .../delegations/models/delegation.model.ts | 10 ++++++++++ 3 files changed, 34 insertions(+) create mode 100644 libs/auth-api-lib/migrations/20240910092516-delegation-reference-id.js diff --git a/libs/auth-api-lib/migrations/20240910092516-delegation-reference-id.js b/libs/auth-api-lib/migrations/20240910092516-delegation-reference-id.js new file mode 100644 index 000000000000..1adacfa56b50 --- /dev/null +++ b/libs/auth-api-lib/migrations/20240910092516-delegation-reference-id.js @@ -0,0 +1,19 @@ +'use strict' + +module.exports = { + async up(queryInterface, Sequelize) { + return Promise.all([ + queryInterface.addColumn('delegation', 'reference_id', { + type: Sequelize.STRING, + allowNull: true, + unique: true, + }), + ]) + }, + + async down(queryInterface, Sequelize) { + return Promise.all([ + queryInterface.removeColumn('delegation', 'reference_id'), + ]) + }, +} diff --git a/libs/auth-api-lib/src/lib/delegations/dto/delegation.dto.ts b/libs/auth-api-lib/src/lib/delegations/dto/delegation.dto.ts index 2dcabc5e4ffc..ac4b4ef96e97 100644 --- a/libs/auth-api-lib/src/lib/delegations/dto/delegation.dto.ts +++ b/libs/auth-api-lib/src/lib/delegations/dto/delegation.dto.ts @@ -63,6 +63,11 @@ export class DelegationDTO { }) provider!: AuthDelegationProvider + @IsOptional() + @ApiPropertyOptional({ nullable: true, type: String }) + @IsString() + referenceId?: string | null + @IsOptional() @ApiPropertyOptional({ type: [DelegationScopeDTO] }) @IsArray() diff --git a/libs/auth-api-lib/src/lib/delegations/models/delegation.model.ts b/libs/auth-api-lib/src/lib/delegations/models/delegation.model.ts index 3106d0fa4354..063ec5bbb448 100644 --- a/libs/auth-api-lib/src/lib/delegations/models/delegation.model.ts +++ b/libs/auth-api-lib/src/lib/delegations/models/delegation.model.ts @@ -80,6 +80,16 @@ export class Delegation extends Model< @ForeignKey(() => Domain) domainName!: CreationOptional + /** + * ReferenceId is a field for storing a reference to the zendesk ticket id + */ + @Column({ + type: DataType.STRING, + allowNull: true, + unique: true, + }) + referenceId?: string + get validTo(): Date | null | undefined { // 1. Find a value with null as validTo. Null means that delegation scope set valid not to a specific time period const withNullValue = this.delegationScopes?.find((x) => x.validTo === null) From 3101e8695179b51c18eaf6fbe6865300fc07039a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9E=C3=B3rey=20J=C3=B3na?= Date: Mon, 16 Sep 2024 17:22:03 +0000 Subject: [PATCH 15/27] fix(native-app): fix android crash due to borderRadius set to string value (#16009) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../screens/vehicles/components/vehicle-item.tsx | 15 ++++++++------- .../src/ui/lib/skeleton/general-card-skeleton.tsx | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/apps/native/app/src/screens/vehicles/components/vehicle-item.tsx b/apps/native/app/src/screens/vehicles/components/vehicle-item.tsx index da31cff6935e..0ac33169fe71 100644 --- a/apps/native/app/src/screens/vehicles/components/vehicle-item.tsx +++ b/apps/native/app/src/screens/vehicles/components/vehicle-item.tsx @@ -2,7 +2,7 @@ import { Label, VehicleCard } from '@ui' import React from 'react' import { FormattedDate, FormattedMessage } from 'react-intl' import { SafeAreaView, TouchableHighlight, View, ViewStyle } from 'react-native' -import { useTheme } from 'styled-components/native' +import styled, { useTheme } from 'styled-components/native' import { ListVehiclesQuery } from '../../../graphql/types/schema' import { navigateTo } from '../../../lib/deep-linking' @@ -14,6 +14,11 @@ type VehicleListItem = NonNullable< NonNullable['vehicleList'] >[0] +const Cell = styled(TouchableHighlight)` + margin-bottom: ${({ theme }) => theme.spacing[2]}; + border-radius: ${({ theme }) => theme.border.radius.extraLarge}; +` + export const VehicleItem = React.memo( ({ item, @@ -39,14 +44,10 @@ export const VehicleItem = React.memo( return ( - { navigateTo(`/vehicle/`, { id: item.permno, @@ -78,7 +79,7 @@ export const VehicleItem = React.memo( } /> - + ) }, diff --git a/apps/native/app/src/ui/lib/skeleton/general-card-skeleton.tsx b/apps/native/app/src/ui/lib/skeleton/general-card-skeleton.tsx index f64d3c68e0e4..8bbd1552977a 100644 --- a/apps/native/app/src/ui/lib/skeleton/general-card-skeleton.tsx +++ b/apps/native/app/src/ui/lib/skeleton/general-card-skeleton.tsx @@ -18,7 +18,7 @@ export const GeneralCardSkeleton = ({ height }: { height: number }) => { overlayOpacity={1} height={height} style={{ - borderRadius: theme.spacing[2], + borderRadius: 16, marginBottom: theme.spacing[2], }} /> From 8a1b601bce0348d1548310746d0e0e505eedd77e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9E=C3=B3rey=20J=C3=B3na?= Date: Mon, 16 Sep 2024 17:28:19 +0000 Subject: [PATCH 16/27] fix(native-app): optional chaining for content.value and data.[0].mileage (#16003) * fix: optional chaining for content.value * fix: optional chaining for mileage as well * fix: better fix --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../screens/document-detail/document-detail.tsx | 15 ++++++++------- .../screens/vehicles/vehicle-mileage.screen.tsx | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/apps/native/app/src/screens/document-detail/document-detail.tsx b/apps/native/app/src/screens/document-detail/document-detail.tsx index 709b06934fa6..e88c3deae612 100644 --- a/apps/native/app/src/screens/document-detail/document-detail.tsx +++ b/apps/native/app/src/screens/document-detail/document-detail.tsx @@ -374,13 +374,14 @@ export const DocumentDetailScreen: NavigationFunctionComponent<{ (isHtml ? ( tags to fix a bug in react-native that renders
with too much vertical space - // https://github.com/facebook/react-native/issues/32062 - `${htmlStyles}${Document.content?.value.replaceAll( - regexForBr, - '', - )}` ?? '', + html: Document.content?.value + ? // Removing all
tags to fix a bug in react-native that renders
with too much vertical space + // https://github.com/facebook/react-native/issues/32062 + `${htmlStyles}${Document.content?.value.replaceAll( + regexForBr, + '', + )}` + : '', }} scalesPageToFit onLoadEnd={() => { diff --git a/apps/native/app/src/screens/vehicles/vehicle-mileage.screen.tsx b/apps/native/app/src/screens/vehicles/vehicle-mileage.screen.tsx index ec360f12d7ef..e6bcd2c9ddf6 100644 --- a/apps/native/app/src/screens/vehicles/vehicle-mileage.screen.tsx +++ b/apps/native/app/src/screens/vehicles/vehicle-mileage.screen.tsx @@ -113,7 +113,7 @@ export const VehicleMileageScreen: NavigationFunctionComponent<{ }, [res.data, res.loading]) const latestMileage = - data?.[0]?.__typename !== 'Skeleton' && data[0].mileage + data?.[0]?.__typename !== 'Skeleton' && data[0]?.mileage ? // Parse mileage from string to number +data[0].mileage : 0 From 63efe4ab9fe634070826f9bff87e63e342a5c1ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9E=C3=B3rey=20J=C3=B3na?= Date: Mon, 16 Sep 2024 17:33:11 +0000 Subject: [PATCH 17/27] fix(native-app): view pager now swipes correct size of elements in home widgets (#15977) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- apps/native/app/src/screens/home/air-discount-module.tsx | 7 +++++-- apps/native/app/src/screens/home/applications-module.tsx | 8 ++++++-- apps/native/app/src/screens/home/licenses-module.tsx | 7 +++++-- apps/native/app/src/screens/home/vehicles-module.tsx | 7 +++++-- apps/native/app/src/ui/lib/view-pager/view-pager.tsx | 4 ++-- 5 files changed, 23 insertions(+), 10 deletions(-) diff --git a/apps/native/app/src/screens/home/air-discount-module.tsx b/apps/native/app/src/screens/home/air-discount-module.tsx index 0f9a473f9ff9..bbabf15460d4 100644 --- a/apps/native/app/src/screens/home/air-discount-module.tsx +++ b/apps/native/app/src/screens/home/air-discount-module.tsx @@ -75,6 +75,7 @@ const AirDiscountModule = React.memo( ) const count = discounts?.length ?? 0 + const viewPagerItemWidth = screenWidth - theme.spacing[2] * 4 const items = discounts?.slice(0, 3).map(({ discountCode, user }) => ( 1 ? { - width: screenWidth - theme.spacing[2] * 4, + width: viewPagerItemWidth, marginLeft: theme.spacing[2], } : { @@ -151,7 +152,9 @@ const AirDiscountModule = React.memo( /> )} {count === 1 && items} - {count >= 2 && {items}} + {count >= 2 && ( + {items} + )} )} diff --git a/apps/native/app/src/screens/home/applications-module.tsx b/apps/native/app/src/screens/home/applications-module.tsx index 7bc79248ce9b..2c30442b7f25 100644 --- a/apps/native/app/src/screens/home/applications-module.tsx +++ b/apps/native/app/src/screens/home/applications-module.tsx @@ -71,6 +71,8 @@ const ApplicationsModule = React.memo( return null } + const viewPagerItemWidth = screenWidth - theme.spacing[2] * 4 + const items = applications.slice(0, 3).map((application) => ( 1 ? { - width: screenWidth - theme.spacing[2] * 4, + width: viewPagerItemWidth, marginLeft: 16, } : {} @@ -169,7 +171,9 @@ const ApplicationsModule = React.memo( /> )} {count === 1 && items} - {count >= 2 && {items}} + {count >= 2 && ( + {items} + )} )} diff --git a/apps/native/app/src/screens/home/licenses-module.tsx b/apps/native/app/src/screens/home/licenses-module.tsx index 0e282e3221f0..b8a3f9ecb108 100644 --- a/apps/native/app/src/screens/home/licenses-module.tsx +++ b/apps/native/app/src/screens/home/licenses-module.tsx @@ -120,6 +120,7 @@ const LicensesModule = React.memo( const count = licenses?.length ?? 0 + (passport ? 1 : 0) const allLicenses = [...(licenses ?? []), ...(passport ?? [])] + const viewPagerItemWidth = screenWidth - theme.spacing[2] * 3 const items = allLicenses .filter( @@ -135,7 +136,7 @@ const LicensesModule = React.memo( style={ count > 1 ? { - width: screenWidth - theme.spacing[2] * 3, + width: viewPagerItemWidth, paddingLeft: theme.spacing[2], paddingRight: 0, } @@ -201,7 +202,9 @@ const LicensesModule = React.memo( /> )} {count === 1 && items} - {count >= 2 && {items}} + {count >= 2 && ( + {items} + )} )} diff --git a/apps/native/app/src/screens/home/vehicles-module.tsx b/apps/native/app/src/screens/home/vehicles-module.tsx index 16d5f1a3b4c2..6e913117e430 100644 --- a/apps/native/app/src/screens/home/vehicles-module.tsx +++ b/apps/native/app/src/screens/home/vehicles-module.tsx @@ -86,6 +86,7 @@ const VehiclesModule = React.memo( return null } + const viewPagerItemWidth = screenWidth - theme.spacing[2] * 3 const count = reorderedVehicles?.length ?? 0 const items = reorderedVehicles?.slice(0, 3).map((vehicle, index) => ( @@ -97,7 +98,7 @@ const VehiclesModule = React.memo( style={ count > 1 ? { - width: screenWidth - theme.spacing[2] * 3, + width: viewPagerItemWidth, paddingHorizontal: 0, paddingLeft: theme.spacing[2], minHeight: 176, @@ -158,7 +159,9 @@ const VehiclesModule = React.memo( /> )} {count === 1 && items} - {count >= 2 && {items}} + {count >= 2 && ( + {items} + )} )} diff --git a/apps/native/app/src/ui/lib/view-pager/view-pager.tsx b/apps/native/app/src/ui/lib/view-pager/view-pager.tsx index a71cccafd6c3..f5e2ffc992bc 100644 --- a/apps/native/app/src/ui/lib/view-pager/view-pager.tsx +++ b/apps/native/app/src/ui/lib/view-pager/view-pager.tsx @@ -62,12 +62,12 @@ export function ViewPager({ children, itemWidth }: ViewPagerProps) { contentWidth - OFFSET - OFFSET_CARD, contentWidth - OFFSET - 60, contentWidth - 120, - ] + ].sort((a, b) => a - b) // Make sure inputRange is non-decreasing to prevent crash : [ OFFSET * i - OFFSET, OFFSET * i, i === pages - 2 ? contentWidth - OFFSET - 60 : OFFSET * i + OFFSET, - ] + ].sort((a, b) => a - b) // Make sure inputRange is non-decreasing to prevent crash const x = useRef(new Animated.Value(0)).current From 51d17c1377200bae09fba63e00433fbe6556b293 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9E=C3=B3rey=20J=C3=B3na?= Date: Mon, 16 Sep 2024 17:38:05 +0000 Subject: [PATCH 18/27] fix(native-app): decrease size of vehicle card on home page (#15972) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- apps/native/app/src/screens/home/vehicles-module.tsx | 4 ++-- apps/native/app/src/ui/lib/card/vehicle-card.tsx | 8 +++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/apps/native/app/src/screens/home/vehicles-module.tsx b/apps/native/app/src/screens/home/vehicles-module.tsx index 6e913117e430..02975f50a8e2 100644 --- a/apps/native/app/src/screens/home/vehicles-module.tsx +++ b/apps/native/app/src/screens/home/vehicles-module.tsx @@ -94,14 +94,14 @@ const VehiclesModule = React.memo( key={vehicle.permno} item={vehicle} index={index} - minHeight={176} + minHeight={152} style={ count > 1 ? { width: viewPagerItemWidth, paddingHorizontal: 0, paddingLeft: theme.spacing[2], - minHeight: 176, + minHeight: 152, } : { width: '100%', diff --git a/apps/native/app/src/ui/lib/card/vehicle-card.tsx b/apps/native/app/src/ui/lib/card/vehicle-card.tsx index 1c4fa43af59d..567452a28590 100644 --- a/apps/native/app/src/ui/lib/card/vehicle-card.tsx +++ b/apps/native/app/src/ui/lib/card/vehicle-card.tsx @@ -64,7 +64,13 @@ export function VehicleCard({ return ( - {title} + + {title} + {color} - {number} From 28c73a001f5c3bf8b4651124429b425a8874b561 Mon Sep 17 00:00:00 2001 From: mannipje <135017126+mannipje@users.noreply.github.com> Date: Mon, 16 Sep 2024 17:50:53 +0000 Subject: [PATCH 19/27] feat(web): Add default header for landlaeknir organization (#16023) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../Organization/Wrapper/OrganizationWrapper.css.ts | 12 ++++++++++++ .../Organization/Wrapper/OrganizationWrapper.tsx | 11 ++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/apps/web/components/Organization/Wrapper/OrganizationWrapper.css.ts b/apps/web/components/Organization/Wrapper/OrganizationWrapper.css.ts index 9c260ce38f54..fdd1d3eb1b43 100644 --- a/apps/web/components/Organization/Wrapper/OrganizationWrapper.css.ts +++ b/apps/web/components/Organization/Wrapper/OrganizationWrapper.css.ts @@ -33,3 +33,15 @@ export const sakHeaderGridContainer = style({ }, }), }) + +export const landlaeknirHeaderGridContainer = style({ + display: 'grid', + maxWidth: '1342px', + margin: '0 auto', + ...themeUtils.responsiveStyle({ + lg: { + gridTemplateRows: '315px', + gridTemplateColumns: '60fr 40fr', + }, + }), +}) diff --git a/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx b/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx index 7505cc6e9795..975d1d3e2129 100644 --- a/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx +++ b/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx @@ -331,7 +331,16 @@ export const OrganizationHeader: React.FC< /> ) case 'landlaeknir': - return ( + return n('usingDefaultHeader', false) ? ( + + ) : ( Date: Mon, 16 Sep 2024 18:15:20 +0000 Subject: [PATCH 20/27] fix(auth-admin-api): Fix delegation type reset when adding new type (#16016) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- libs/auth-api-lib/src/lib/clients/clients.service.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/libs/auth-api-lib/src/lib/clients/clients.service.ts b/libs/auth-api-lib/src/lib/clients/clients.service.ts index 29cd01e9b13b..b1376f211c55 100644 --- a/libs/auth-api-lib/src/lib/clients/clients.service.ts +++ b/libs/auth-api-lib/src/lib/clients/clients.service.ts @@ -55,9 +55,7 @@ export class ClientsService { private readonly clientDelegationType: typeof ClientDelegationType, @InjectModel(ClientPostLogoutRedirectUri) private clientPostLogoutUri: typeof ClientPostLogoutRedirectUri, - private readonly clientsTranslationService: ClientsTranslationService, - @Inject(LOGGER_PROVIDER) private logger: Logger, ) {} @@ -606,10 +604,12 @@ export class ClientsService { ) { await this.clientModel.update( { - supportsCustomDelegation, - supportsLegalGuardians, - supportsProcuringHolders, - supportsPersonalRepresentatives, + ...(supportsLegalGuardians ? { supportsLegalGuardians } : {}), + ...(supportsPersonalRepresentatives + ? { supportsPersonalRepresentatives } + : {}), + ...(supportsProcuringHolders ? { supportsProcuringHolders } : {}), + ...(supportsCustomDelegation ? { supportsCustomDelegation } : {}), }, { ...options, From a5c9f5c229cac831ebf2e9a560c9410a71a9872a Mon Sep 17 00:00:00 2001 From: juni-haukur <158475136+juni-haukur@users.noreply.github.com> Date: Mon, 16 Sep 2024 18:30:19 +0000 Subject: [PATCH 21/27] feat(service-portal): Signee view for parliamentary signature collection (#16019) * initial setup parliamentary * Parliamentary signee view * update urls for parliamentary * feedback * more * refactor is parliamentary check --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../src/forms/Done.ts | 2 +- .../src/forms/Done.ts | 2 +- .../src/hooks/graphql/queries.ts | 1 + .../OwnerView/ViewList/Signees.tsx | 5 +- .../screens/Parliamentary/OwnerView/index.tsx | 17 +- .../Parliamentary/SignedList/index.tsx | 146 ++++++++++++++++++ .../Parliamentary/SigneeView/index.tsx | 126 +++++++++++++-- .../src/screens/Parliamentary/index.tsx | 45 ++++-- 8 files changed, 314 insertions(+), 30 deletions(-) create mode 100644 libs/service-portal/signature-collection/src/screens/Parliamentary/SignedList/index.tsx diff --git a/libs/application/templates/signature-collection/parliamentary-list-creation/src/forms/Done.ts b/libs/application/templates/signature-collection/parliamentary-list-creation/src/forms/Done.ts index b649ebb9d418..267416ed6140 100644 --- a/libs/application/templates/signature-collection/parliamentary-list-creation/src/forms/Done.ts +++ b/libs/application/templates/signature-collection/parliamentary-list-creation/src/forms/Done.ts @@ -62,7 +62,7 @@ export const Done: Form = buildForm({ buildMessageWithLinkButtonField({ id: 'done.goToServicePortal', title: '', - url: '/minarsidur/min-gogn/listar/medmaelasofnun', + url: '/minarsidur/min-gogn/listar/althingis-medmaelasofnun', buttonTitle: m.linkFieldButtonTitle, message: m.linkFieldMessage, }), diff --git a/libs/application/templates/signature-collection/parliamentary-list-signing/src/forms/Done.ts b/libs/application/templates/signature-collection/parliamentary-list-signing/src/forms/Done.ts index 746305678d9c..e65e617c9965 100644 --- a/libs/application/templates/signature-collection/parliamentary-list-signing/src/forms/Done.ts +++ b/libs/application/templates/signature-collection/parliamentary-list-signing/src/forms/Done.ts @@ -45,7 +45,7 @@ export const Done: Form = buildForm({ buildMessageWithLinkButtonField({ id: 'done.goToServicePortal', title: '', - url: '/minarsidur/min-gogn/listar/medmaelasofnun', + url: '/minarsidur/min-gogn/listar/althingis-medmaelasofnun', buttonTitle: m.linkFieldButtonTitle, message: m.linkFieldMessage, }), diff --git a/libs/service-portal/signature-collection/src/hooks/graphql/queries.ts b/libs/service-portal/signature-collection/src/hooks/graphql/queries.ts index b3ef1936d8b2..55a7b6a24cb9 100644 --- a/libs/service-portal/signature-collection/src/hooks/graphql/queries.ts +++ b/libs/service-portal/signature-collection/src/hooks/graphql/queries.ts @@ -144,6 +144,7 @@ export const GetCurrentCollection = gql` startTime name isActive + isPresidential status areas { id diff --git a/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees.tsx b/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees.tsx index 00407d53f316..93a93e8fd759 100644 --- a/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees.tsx +++ b/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees.tsx @@ -19,7 +19,10 @@ const Signees = () => { useNamespaces('sp.signatureCollection') const { formatMessage } = useLocale() const { pathname } = useLocation() - const listId = pathname.replace('/min-gogn/listar/medmaelasofnun/', '') + const listId = pathname.replace( + '/min-gogn/listar/althingis-medmaelasofnun/', + '', + ) const [searchTerm, setSearchTerm] = useState('') const { listSignees, loadingSignees } = useGetListSignees(listId) diff --git a/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/index.tsx b/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/index.tsx index b1f00d7303e9..5337b7b86c23 100644 --- a/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/index.tsx +++ b/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/index.tsx @@ -14,10 +14,25 @@ import { useLocale } from '@island.is/localization' import { m } from '../../../lib/messages' import AddConstituency from './modals/AddConstituency' import DeletePerson from './modals/DeletePerson' +import { + useGetListsForOwner, + useGetListsForUser, + useIsOwner, +} from '../../../hooks' +import { useAuth } from '@island.is/auth/react' +import { SignatureCollection } from '@island.is/api/schema' -const OwnerView = () => { +const OwnerView = ({ + currentCollection, +}: { + currentCollection: SignatureCollection +}) => { const navigate = useNavigate() const { formatMessage } = useLocale() + const { userInfo: user } = useAuth() + const { listsForOwner, loadingOwnerLists } = useGetListsForOwner( + currentCollection?.id || '', + ) return ( diff --git a/libs/service-portal/signature-collection/src/screens/Parliamentary/SignedList/index.tsx b/libs/service-portal/signature-collection/src/screens/Parliamentary/SignedList/index.tsx new file mode 100644 index 000000000000..7317f2874728 --- /dev/null +++ b/libs/service-portal/signature-collection/src/screens/Parliamentary/SignedList/index.tsx @@ -0,0 +1,146 @@ +import { ActionCard, Box, Button, Text, toast } from '@island.is/island-ui/core' +import { useLocale } from '@island.is/localization' +import { m } from '../../../lib/messages' +import { Modal } from '@island.is/service-portal/core' +import { useState } from 'react' +import { useGetSignedList } from '../../../hooks' +import format from 'date-fns/format' +import { useMutation } from '@apollo/client' +import { unSignList } from '../../../hooks/graphql/mutations' +import { + SignatureCollectionSignedList, + SignatureCollectionSuccess, +} from '@island.is/api/schema' + +const SignedList = () => { + const { formatMessage } = useLocale() + const [modalIsOpen, setModalIsOpen] = useState(false) + + // SignedList is typically singular, although it may consist of multiple entries, which in that case will all be invalid + const { signedLists, loadingSignedLists, refetchSignedLists } = + useGetSignedList() + + const [unSign, { loading }] = useMutation(unSignList, { + variables: { + input: { + listId: + signedLists && signedLists?.length === 1 + ? signedLists[0].id + : undefined, + }, + }, + }) + + const onUnSignList = async () => { + try { + await unSign().then(({ data }) => { + if ( + ( + data as unknown as { + signatureCollectionUnsign: SignatureCollectionSuccess + } + ).signatureCollectionUnsign.success + ) { + toast.success(formatMessage(m.unSignSuccess)) + setModalIsOpen(false) + refetchSignedLists() + } else { + setModalIsOpen(false) + } + }) + } catch (e) { + toast.error(formatMessage(m.unSignError)) + } + } + + return ( + + {!loadingSignedLists && !!signedLists?.length && ( + + {formatMessage(m.mySigneeListsHeader)} + {signedLists?.map((list: SignatureCollectionSignedList) => { + return ( + + setModalIsOpen(true), + icon: undefined, + } + : undefined + } + tag={ + list.isValid && !list.active + ? { + label: formatMessage(m.collectionClosed), + variant: 'red', + outlined: true, + } + : list.isValid && !list.isDigital + ? { + label: formatMessage(m.paperUploadedSignature), + variant: 'blue', + outlined: true, + } + : !list.isValid + ? { + label: formatMessage(m.signatureIsInvalid), + variant: 'red', + outlined: false, + } + : undefined + } + /> + setModalIsOpen(false)} + > + + {formatMessage(m.unSignList)} + + + {formatMessage(m.unSignModalMessage)} + + + + + + + ) + })} + + )} + + ) +} + +export default SignedList diff --git a/libs/service-portal/signature-collection/src/screens/Parliamentary/SigneeView/index.tsx b/libs/service-portal/signature-collection/src/screens/Parliamentary/SigneeView/index.tsx index 1701ddf89f7d..78cb34ff9c49 100644 --- a/libs/service-portal/signature-collection/src/screens/Parliamentary/SigneeView/index.tsx +++ b/libs/service-portal/signature-collection/src/screens/Parliamentary/SigneeView/index.tsx @@ -1,26 +1,120 @@ -import { ActionCard, Box, Text } from '@island.is/island-ui/core' +import { + ActionCard, + AlertMessage, + Box, + Button, + Stack, + Text, +} from '@island.is/island-ui/core' import { useLocale } from '@island.is/localization' +import { EmptyState } from '@island.is/service-portal/core' +import { useGetListsForUser, useGetSignedList } from '../../../hooks' +import format from 'date-fns/format' +import { Skeleton } from '../../../skeletons' +import { useAuth } from '@island.is/auth/react' +import { sortAlpha } from '@island.is/shared/utils' import { m } from '../../../lib/messages' +import SignedList from '../SignedList' +import { SignatureCollection } from '../../../types/schema' + +const SigneeView = ({ + currentCollection, +}: { + currentCollection: SignatureCollection +}) => { + const { userInfo: user } = useAuth() -const SigneeView = () => { const { formatMessage } = useLocale() + const { signedLists, loadingSignedLists } = useGetSignedList() + const { listsForUser, loadingUserLists } = useGetListsForUser( + currentCollection?.id, + ) return ( - - {formatMessage(m.mySigneeListsHeader)} - - + {!user?.profile.actor && !loadingSignedLists && !loadingUserLists ? ( + + {currentCollection.isPresidential && + listsForUser.length === 0 && + signedLists.length === 0 && ( + + + + )} + + {/* Signed list */} + + + {/* Other available lists */} + + {listsForUser.length > 0 && ( + + {formatMessage(m.mySigneeListsByAreaHeader)} + + )} + + + {[...listsForUser]?.sort(sortAlpha('title')).map((list) => { + return ( + new Date() && !list.maxReached + ? { + label: formatMessage(m.signList), + variant: 'text', + icon: 'arrowForward', + disabled: !!signedLists.length, + onClick: () => { + window.open( + `${document.location.origin}${list.slug}`, + ) + }, + } + : undefined + } + tag={ + new Date(list.endTime) < new Date() + ? { + label: formatMessage(m.collectionClosed), + variant: 'red', + outlined: true, + } + : list.maxReached + ? { + label: formatMessage(m.collectionMaxReached), + variant: 'red', + outlined: true, + } + : undefined + } + /> + ) + })} + + + + + ) : user?.profile.actor ? ( + + ) : ( + + )} ) } diff --git a/libs/service-portal/signature-collection/src/screens/Parliamentary/index.tsx b/libs/service-portal/signature-collection/src/screens/Parliamentary/index.tsx index 3ae328e2972a..e84d9872468d 100644 --- a/libs/service-portal/signature-collection/src/screens/Parliamentary/index.tsx +++ b/libs/service-portal/signature-collection/src/screens/Parliamentary/index.tsx @@ -1,24 +1,49 @@ import { Box } from '@island.is/island-ui/core' import { useLocale } from '@island.is/localization' -import { IntroHeader, THJODSKRA_SLUG } from '@island.is/service-portal/core' +import { + EmptyState, + IntroHeader, + THJODSKRA_SLUG, +} from '@island.is/service-portal/core' import { m } from '../../lib/messages' import OwnerView from './OwnerView' import SigneeView from './SigneeView' +import { useGetCurrentCollection, useIsOwner } from '../../hooks' +import { Skeleton } from '../../skeletons' const SignatureListsParliamentary = () => { const { formatMessage } = useLocale() + const { isOwner, loadingIsOwner } = useIsOwner() + const { currentCollection, loadingCurrentCollection } = + useGetCurrentCollection() + return ( - - - - + + {!loadingIsOwner && !loadingCurrentCollection ? ( + + {!currentCollection?.isPresidential ? ( + isOwner.success ? ( + + ) : ( + + ) + ) : ( + + )} + + ) : ( + + )} ) } From 01b8904823c96a7ad65f2668f108d5d174a4b871 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81sd=C3=ADs=20Erna=20Gu=C3=B0mundsd=C3=B3ttir?= Date: Mon, 16 Sep 2024 18:48:28 +0000 Subject: [PATCH 22/27] fix(service-portal): Health Spacing (#16020) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../health/src/screens/OrganDonation/OrganDonation.tsx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/libs/service-portal/health/src/screens/OrganDonation/OrganDonation.tsx b/libs/service-portal/health/src/screens/OrganDonation/OrganDonation.tsx index 275e6b0fd25b..ba3501ea7d22 100644 --- a/libs/service-portal/health/src/screens/OrganDonation/OrganDonation.tsx +++ b/libs/service-portal/health/src/screens/OrganDonation/OrganDonation.tsx @@ -36,7 +36,7 @@ const OrganDonation = () => { title={formatMessage(m.organDonation)} intro={formatMessage(m.organDonationDescription)} /> - + { )} {!error && !loading && donorStatus !== null && ( - + {formatMessage(m.takeOnOrganDonation)} Date: Mon, 16 Sep 2024 18:57:30 +0000 Subject: [PATCH 23/27] feat(web): Add default header for natturuhamfaratryggingar islands (#16022) * Add default header for natturuhamfaratryggingar islands * Remove image position prop * Namespace for image in default header --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../Organization/Wrapper/OrganizationWrapper.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx b/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx index 975d1d3e2129..873637509e02 100644 --- a/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx +++ b/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx @@ -435,7 +435,15 @@ export const OrganizationHeader: React.FC< /> ) case 'nti': - return ( + return n('usingDefaultHeader', false) ? ( + + ) : ( Date: Mon, 16 Sep 2024 19:17:20 +0000 Subject: [PATCH 24/27] fix(occupational-licenses): Remove date of birth for health directorate licenses (#16025) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../src/lib/occupationalLicensesV2.service.ts | 1 - .../OccupationalLicensesDetail/OccupationalLicensesDetail.tsx | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/libs/api/domains/occupational-licenses-v2/src/lib/occupationalLicensesV2.service.ts b/libs/api/domains/occupational-licenses-v2/src/lib/occupationalLicensesV2.service.ts index 55abcf0b71da..4deb5bd691df 100644 --- a/libs/api/domains/occupational-licenses-v2/src/lib/occupationalLicensesV2.service.ts +++ b/libs/api/domains/occupational-licenses-v2/src/lib/occupationalLicensesV2.service.ts @@ -268,7 +268,6 @@ export class OccupationalLicensesV2Service { permit: data.practice, licenseHolderName: data.licenseHolderName, licenseHolderNationalId: data.licenseHolderNationalId, - dateOfBirth: info(data.licenseHolderNationalId).birthday, validFrom: data.validFrom, title: `${data.profession} - ${data.practice}`, status, diff --git a/libs/service-portal/occupational-licenses/src/screens/v2/OccupationalLicensesDetail/OccupationalLicensesDetail.tsx b/libs/service-portal/occupational-licenses/src/screens/v2/OccupationalLicensesDetail/OccupationalLicensesDetail.tsx index ecc45395cf6b..7a3fc03c9140 100644 --- a/libs/service-portal/occupational-licenses/src/screens/v2/OccupationalLicensesDetail/OccupationalLicensesDetail.tsx +++ b/libs/service-portal/occupational-licenses/src/screens/v2/OccupationalLicensesDetail/OccupationalLicensesDetail.tsx @@ -140,7 +140,7 @@ const OccupationalLicenseDetail = () => { content={license?.licenseNumber ?? ''} /> )} - {(license?.dateOfBirth || loading) && ( + {license?.dateOfBirth && ( Date: Mon, 16 Sep 2024 19:37:17 +0000 Subject: [PATCH 25/27] fix(application-plc): Stop double candidacies in dataprovider (#16026) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../parliamentary-list-creation.service.ts | 11 +++++++++++ .../parliamentary-list-creation/src/lib/errors.ts | 12 ++++++++++++ 2 files changed, 23 insertions(+) diff --git a/libs/application/template-api-modules/src/lib/modules/templates/signature-collection/parliamentary-list-creation/parliamentary-list-creation.service.ts b/libs/application/template-api-modules/src/lib/modules/templates/signature-collection/parliamentary-list-creation/parliamentary-list-creation.service.ts index 22eb6250e522..ef3e980c42e3 100644 --- a/libs/application/template-api-modules/src/lib/modules/templates/signature-collection/parliamentary-list-creation/parliamentary-list-creation.service.ts +++ b/libs/application/template-api-modules/src/lib/modules/templates/signature-collection/parliamentary-list-creation/parliamentary-list-creation.service.ts @@ -53,6 +53,17 @@ export class ParliamentaryListCreationService extends BaseTemplateApiService { 405, ) } + const contactNationalId = isCompany(auth.nationalId) + ? auth.actor?.nationalId ?? auth.nationalId + : auth.nationalId + + if ( + currentCollection.candidates.some( + (c) => c.nationalId.replace('-', '') === contactNationalId, + ) + ) { + throw new TemplateApiError(errorMessages.alreadyCandidate, 412) + } return currentCollection } diff --git a/libs/application/templates/signature-collection/parliamentary-list-creation/src/lib/errors.ts b/libs/application/templates/signature-collection/parliamentary-list-creation/src/lib/errors.ts index 1087cbe59ec2..62d7a65bce9e 100644 --- a/libs/application/templates/signature-collection/parliamentary-list-creation/src/lib/errors.ts +++ b/libs/application/templates/signature-collection/parliamentary-list-creation/src/lib/errors.ts @@ -26,6 +26,18 @@ export const errorMessages = { description: '', }, }), + alreadyCandidate: defineMessages({ + title: { + id: 'plc.application:error.alreadyCandidate.title', + defaultMessage: 'Ekki hægt að tvískrá meðmælasöfnun', + description: '', + }, + summary: { + id: 'plc.application:error.alreadyCandidate.summary', + defaultMessage: 'Þú ert nú þegar með framboð', + description: '', + }, + }), citizenship: defineMessages({ title: { id: 'plc.application:error.citizenship.title', From 43a6c6db3964722a12cde69b6612c13cd3492911 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9E=C3=B3rey=20J=C3=B3na?= Date: Mon, 16 Sep 2024 19:48:50 +0000 Subject: [PATCH 26/27] fix(native-app): fix vehicle details screen transition (#15970) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- apps/native/app/src/utils/lifecycle/setup-routes.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/native/app/src/utils/lifecycle/setup-routes.ts b/apps/native/app/src/utils/lifecycle/setup-routes.ts index a56a0bd2fb4e..54a7d80a3344 100644 --- a/apps/native/app/src/utils/lifecycle/setup-routes.ts +++ b/apps/native/app/src/utils/lifecycle/setup-routes.ts @@ -222,7 +222,6 @@ export function setupRoutes() { addRoute('/vehicle/:id', async (passProps: any) => { await Navigation.dismissAllModals() selectTab(4) - await Navigation.popToRoot(StackRegistry.MoreStack) Navigation.push(ComponentRegistry.MoreScreen, { component: { name: ComponentRegistry.VehicleDetailScreen, From c2ab2f5400df8f94bd1f74cfdd30c1a716e5a558 Mon Sep 17 00:00:00 2001 From: unakb Date: Mon, 16 Sep 2024 20:21:19 +0000 Subject: [PATCH 27/27] feat(j-s): Subpoena delivery and endpoint for updates (#15918) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(j-s): New endpoints for subpoenas in xrd-api * test(j-s): Tests * feat(j-s): WIP create subpoena * feat(j-s): New subpoena table * chore(j-s): Add institution addresses to db * chore(j-s): Use institution from db for subpoena * chore(j-s): Subpoena work * fix(j-s): remove unused code * Update case.service.ts * Update message.service.ts * fix(j-s): Renaming and exception handling * Delete 20240815104210-update-explanatory_comment.js * fix(j-s): Complete rewrite to create a new subpoena module to handle subpoenas Rather than having the logic all over and confusing * Cleanup * fix(j-s): More cleanup * feat(j-s): Update defendant info * feat(j-s): Add guards * fix(j-s): cleanup * Update app.service.ts * Update app.service.ts * fix(j-s): Removed unused files * Update subpoena.dto.ts * fix(j-s): Feedback resolved * fix(j-s): cleanup * fix(j-s): review fixes * feat(j-s): Add field for registered by --------- Co-authored-by: Guðjón Guðjónsson Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../20240827132504-create-subpoena.js | 72 +++++++++ .../backend/src/app/app.module.ts | 2 + .../src/app/modules/case/case.service.ts | 4 +- .../defendant/internalDefendant.controller.ts | 6 +- .../backend/src/app/modules/index.ts | 1 + .../notification/notification.service.ts | 9 ++ .../police/models/createSubpoena.response.ts | 6 + .../src/app/modules/police/police.service.ts | 71 +++++++++ .../app/modules/subpoena/dto/deliver.dto.ts | 12 ++ .../subpoena/dto/updateSubpoena.dto.ts | 47 ++++++ .../subpoena/guards/subpoena.decorator.ts | 7 + .../subpoena/guards/subpoenaExists.guard.ts | 27 ++++ .../subpoena/internalSubpoena.controller.ts | 110 ++++++++++++++ .../subpoena/models/deliver.response.ts | 6 + .../modules/subpoena/models/subpoena.model.ts | 71 +++++++++ .../app/modules/subpoena/subpoena.module.ts | 22 +++ .../app/modules/subpoena/subpoena.service.ts | 140 ++++++++++++++++++ .../xrd-api/src/app/app.controller.ts | 16 ++ .../xrd-api/src/app/app.service.ts | 103 ++++++++++++- .../xrd-api/src/app/dto/subpoena.dto.ts | 32 ++++ .../src/app/models/subpoena.response.ts | 22 +++ .../formatters/src/lib/formatters.ts | 2 +- .../message/src/lib/message.ts | 2 + 23 files changed, 778 insertions(+), 12 deletions(-) create mode 100644 apps/judicial-system/backend/migrations/20240827132504-create-subpoena.js create mode 100644 apps/judicial-system/backend/src/app/modules/police/models/createSubpoena.response.ts create mode 100644 apps/judicial-system/backend/src/app/modules/subpoena/dto/deliver.dto.ts create mode 100644 apps/judicial-system/backend/src/app/modules/subpoena/dto/updateSubpoena.dto.ts create mode 100644 apps/judicial-system/backend/src/app/modules/subpoena/guards/subpoena.decorator.ts create mode 100644 apps/judicial-system/backend/src/app/modules/subpoena/guards/subpoenaExists.guard.ts create mode 100644 apps/judicial-system/backend/src/app/modules/subpoena/internalSubpoena.controller.ts create mode 100644 apps/judicial-system/backend/src/app/modules/subpoena/models/deliver.response.ts create mode 100644 apps/judicial-system/backend/src/app/modules/subpoena/models/subpoena.model.ts create mode 100644 apps/judicial-system/backend/src/app/modules/subpoena/subpoena.module.ts create mode 100644 apps/judicial-system/backend/src/app/modules/subpoena/subpoena.service.ts create mode 100644 apps/judicial-system/xrd-api/src/app/dto/subpoena.dto.ts create mode 100644 apps/judicial-system/xrd-api/src/app/models/subpoena.response.ts diff --git a/apps/judicial-system/backend/migrations/20240827132504-create-subpoena.js b/apps/judicial-system/backend/migrations/20240827132504-create-subpoena.js new file mode 100644 index 000000000000..b5de476d02c9 --- /dev/null +++ b/apps/judicial-system/backend/migrations/20240827132504-create-subpoena.js @@ -0,0 +1,72 @@ +'use strict' + +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.sequelize.transaction((t) => + queryInterface.createTable( + 'subpoena', + { + id: { + type: Sequelize.UUID, + primaryKey: true, + allowNull: false, + defaultValue: Sequelize.UUIDV4, + }, + created: { + type: 'TIMESTAMP WITH TIME ZONE', + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'), + allowNull: false, + }, + modified: { + type: 'TIMESTAMP WITH TIME ZONE', + defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'), + allowNull: false, + }, + defendant_id: { + type: Sequelize.UUID, + references: { + model: 'defendant', + key: 'id', + }, + allowNull: false, + }, + case_id: { + type: Sequelize.UUID, + references: { + model: 'case', + key: 'id', + }, + allowNull: true, + }, + subpoena_id: { + type: Sequelize.STRING, + allowNull: true, + }, + acknowledged: { + type: Sequelize.BOOLEAN, + allowNull: true, + }, + acknowledged_date: { + type: 'TIMESTAMP WITH TIME ZONE', + allowNull: true, + }, + registered_by: { + type: Sequelize.STRING, + allowNull: true, + }, + comment: { + type: Sequelize.TEXT, + allowNull: true, + }, + }, + { transaction: t }, + ), + ) + }, + + down: (queryInterface) => { + return queryInterface.sequelize.transaction((t) => + queryInterface.dropTable('subpoena', { transaction: t }), + ) + }, +} diff --git a/apps/judicial-system/backend/src/app/app.module.ts b/apps/judicial-system/backend/src/app/app.module.ts index 08d478a26b3e..58c55e4b4e5f 100644 --- a/apps/judicial-system/backend/src/app/app.module.ts +++ b/apps/judicial-system/backend/src/app/app.module.ts @@ -30,6 +30,7 @@ import { notificationModuleConfig, PoliceModule, policeModuleConfig, + SubpoenaModule, UserModule, userModuleConfig, } from './modules' @@ -50,6 +51,7 @@ import { SequelizeConfigService } from './sequelizeConfig.service' NotificationModule, PoliceModule, EventLogModule, + SubpoenaModule, ProblemModule.forRoot({ logAllErrors: true }), ConfigModule.forRoot({ isGlobal: true, diff --git a/apps/judicial-system/backend/src/app/modules/case/case.service.ts b/apps/judicial-system/backend/src/app/modules/case/case.service.ts index eabf3a6cee0b..f1ac7a7078fc 100644 --- a/apps/judicial-system/backend/src/app/modules/case/case.service.ts +++ b/apps/judicial-system/backend/src/app/modules/case/case.service.ts @@ -597,7 +597,7 @@ export class CaseService { ] } - private addMessagesForSubmittedIndicitmentCaseToQueue( + private addMessagesForSubmittedIndictmentCaseToQueue( theCase: Case, user: TUser, ): Promise { @@ -1181,7 +1181,7 @@ export class CaseService { await this.addMessagesForCompletedCaseToQueue(updatedCase, user) } } else if (updatedCase.state === CaseState.SUBMITTED && isIndictment) { - await this.addMessagesForSubmittedIndicitmentCaseToQueue( + await this.addMessagesForSubmittedIndictmentCaseToQueue( updatedCase, user, ) diff --git a/apps/judicial-system/backend/src/app/modules/defendant/internalDefendant.controller.ts b/apps/judicial-system/backend/src/app/modules/defendant/internalDefendant.controller.ts index 1fbefa6b9af2..367ac2df85dc 100644 --- a/apps/judicial-system/backend/src/app/modules/defendant/internalDefendant.controller.ts +++ b/apps/judicial-system/backend/src/app/modules/defendant/internalDefendant.controller.ts @@ -65,15 +65,15 @@ export class InternalDefendantController { @Patch('defense/:defendantNationalId') @ApiOkResponse({ type: Defendant, - description: 'Assigns defense choice to defendant', + description: 'Updates defendant information by case and national id', }) - async assignDefender( + async updateDefendant( @Param('caseId') caseId: string, @Param('defendantNationalId') defendantNationalId: string, @CurrentCase() theCase: Case, @Body() updatedDefendantChoice: UpdateDefendantDto, ): Promise { - this.logger.debug(`Assigning defense choice to defendant in case ${caseId}`) + this.logger.debug(`Updating defendant info for ${caseId}`) const updatedDefendant = await this.defendantService.updateByNationalId( theCase.id, diff --git a/apps/judicial-system/backend/src/app/modules/index.ts b/apps/judicial-system/backend/src/app/modules/index.ts index 95a03b26fdc4..d2aee8e277e6 100644 --- a/apps/judicial-system/backend/src/app/modules/index.ts +++ b/apps/judicial-system/backend/src/app/modules/index.ts @@ -18,3 +18,4 @@ export { CourtModule } from './court/court.module' export { AwsS3Module } from './aws-s3/awsS3.module' export { EventModule } from './event/event.module' export { EventLogModule } from './event-log/eventLog.module' +export { SubpoenaModule } from './subpoena/subpoena.module' diff --git a/apps/judicial-system/backend/src/app/modules/notification/notification.service.ts b/apps/judicial-system/backend/src/app/modules/notification/notification.service.ts index 8103b1106f14..e4ffc288777d 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/notification.service.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/notification.service.ts @@ -69,6 +69,15 @@ export class NotificationService { ] } else { messages = [this.getNotificationMessage(type, user, theCase)] + theCase.defendants?.forEach((defendant) => { + // TODO: move this elsewhere when we know exactly where the trigger should be + messages.push({ + type: MessageType.DELIVERY_TO_POLICE_SUBPOENA, + user, + caseId: theCase.id, + elementId: defendant.id, + }) + }) } break case NotificationType.HEADS_UP: diff --git a/apps/judicial-system/backend/src/app/modules/police/models/createSubpoena.response.ts b/apps/judicial-system/backend/src/app/modules/police/models/createSubpoena.response.ts new file mode 100644 index 000000000000..d480c70a4271 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/police/models/createSubpoena.response.ts @@ -0,0 +1,6 @@ +import { ApiProperty } from '@nestjs/swagger' + +export class CreateSubpoenaResponse { + @ApiProperty({ type: String }) + subpoenaId!: string +} diff --git a/apps/judicial-system/backend/src/app/modules/police/police.service.ts b/apps/judicial-system/backend/src/app/modules/police/police.service.ts index 0aed780b4726..ac21cd766a84 100644 --- a/apps/judicial-system/backend/src/app/modules/police/police.service.ts +++ b/apps/judicial-system/backend/src/app/modules/police/police.service.ts @@ -26,8 +26,11 @@ import { CaseState, CaseType } from '@island.is/judicial-system/types' import { nowFactory } from '../../factories' import { AwsS3Service } from '../aws-s3' +import { Case } from '../case' +import { Defendant } from '../defendant/models/defendant.model' import { EventService } from '../event' import { UploadPoliceCaseFileDto } from './dto/uploadPoliceCaseFile.dto' +import { CreateSubpoenaResponse } from './models/createSubpoena.response' import { PoliceCaseFile } from './models/policeCaseFile.model' import { PoliceCaseInfo } from './models/policeCaseInfo.model' import { UploadPoliceCaseFileResponse } from './models/uploadPoliceCaseFile.response' @@ -505,4 +508,72 @@ export class PoliceService { return false }) } + + async createSubpoena( + workingCase: Case, + defendant: Defendant, + subpoena: string, + user: User, + ): Promise { + const { courtCaseNumber, dateLogs, prosecutor, policeCaseNumbers, court } = + workingCase + const { nationalId: defendantNationalId } = defendant + const { name: actor } = user + + const documentName = `Fyrirkall í máli ${workingCase.courtCaseNumber}` + const arraignmentInfo = dateLogs?.find( + (dateLog) => dateLog.dateType === 'ARRAIGNMENT_DATE', + ) + try { + const res = await this.fetchPoliceCaseApi( + `${this.xRoadPath}/CreateSubpoena`, + { + method: 'POST', + headers: { + accept: '*/*', + 'Content-Type': 'application/json', + 'X-Road-Client': this.config.clientId, + 'X-API-KEY': this.config.policeApiKey, + }, + agent: this.agent, + body: JSON.stringify({ + documentName: documentName, + documentBase64: subpoena, + courtRegistrationDate: arraignmentInfo?.date, + prosecutorSsn: prosecutor?.nationalId, + prosecutedSsn: defendantNationalId, + courtAddress: court?.address, + courtRoomNumber: arraignmentInfo?.location || '', + courtCeremony: 'Þingfesting', + lokeCaseNumber: policeCaseNumbers?.[0], + courtCaseNumber: courtCaseNumber, + fileTypeCode: 'BRTNG', + }), + } as RequestInit, + ) + + if (!res.ok) { + throw await res.json() + } + + const subpoenaId = await res.json() + return { subpoenaId } + } catch (error) { + this.logger.error(`Failed create subpoena for case ${workingCase.id}`, { + error, + }) + + this.eventService.postErrorEvent( + 'Failed to create subpoena', + { + caseId: workingCase.id, + defendantId: defendant?.nationalId, + actor, + }, + error, + ) + + throw error + } + } } diff --git a/apps/judicial-system/backend/src/app/modules/subpoena/dto/deliver.dto.ts b/apps/judicial-system/backend/src/app/modules/subpoena/dto/deliver.dto.ts new file mode 100644 index 000000000000..7595952ea24d --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/subpoena/dto/deliver.dto.ts @@ -0,0 +1,12 @@ +import { IsNotEmpty, IsObject } from 'class-validator' + +import { ApiProperty } from '@nestjs/swagger' + +import type { User } from '@island.is/judicial-system/types' + +export class DeliverDto { + @IsNotEmpty() + @IsObject() + @ApiProperty({ type: Object }) + readonly user!: User +} diff --git a/apps/judicial-system/backend/src/app/modules/subpoena/dto/updateSubpoena.dto.ts b/apps/judicial-system/backend/src/app/modules/subpoena/dto/updateSubpoena.dto.ts new file mode 100644 index 000000000000..b482b972be30 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/subpoena/dto/updateSubpoena.dto.ts @@ -0,0 +1,47 @@ +import { IsBoolean, IsEnum, IsOptional, IsString } from 'class-validator' + +import { ApiPropertyOptional } from '@nestjs/swagger' + +import { DefenderChoice } from '@island.is/judicial-system/types' + +export class UpdateSubpoenaDto { + @IsOptional() + @IsBoolean() + @ApiPropertyOptional({ type: Boolean }) + readonly acknowledged?: boolean + + @IsOptional() + @IsString() + @ApiPropertyOptional({ type: String }) + readonly registeredBy?: string + + @IsOptional() + @IsString() + @ApiPropertyOptional({ type: String }) + readonly comment?: string + + @IsOptional() + @IsEnum(DefenderChoice) + @ApiPropertyOptional({ enum: DefenderChoice }) + readonly defenderChoice?: DefenderChoice + + @IsOptional() + @IsString() + @ApiPropertyOptional({ type: String }) + readonly defenderNationalId?: string + + @IsOptional() + @IsString() + @ApiPropertyOptional({ type: String }) + readonly defenderName?: string + + @IsOptional() + @IsString() + @ApiPropertyOptional({ type: String }) + readonly defenderEmail?: string + + @IsOptional() + @IsString() + @ApiPropertyOptional({ type: String }) + readonly defenderPhoneNumber?: string +} diff --git a/apps/judicial-system/backend/src/app/modules/subpoena/guards/subpoena.decorator.ts b/apps/judicial-system/backend/src/app/modules/subpoena/guards/subpoena.decorator.ts new file mode 100644 index 000000000000..a6a39f58182e --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/subpoena/guards/subpoena.decorator.ts @@ -0,0 +1,7 @@ +import { createParamDecorator } from '@nestjs/common' + +import { Subpoena } from '../models/subpoena.model' + +export const CurrentSubpoena = createParamDecorator( + (data, { args: [_1, { req }] }): Subpoena => req.subpoena, +) diff --git a/apps/judicial-system/backend/src/app/modules/subpoena/guards/subpoenaExists.guard.ts b/apps/judicial-system/backend/src/app/modules/subpoena/guards/subpoenaExists.guard.ts new file mode 100644 index 000000000000..0280c3f51f99 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/subpoena/guards/subpoenaExists.guard.ts @@ -0,0 +1,27 @@ +import { + BadRequestException, + CanActivate, + ExecutionContext, + Injectable, +} from '@nestjs/common' + +import { SubpoenaService } from '../subpoena.service' + +@Injectable() +export class SubpoenaExistsGuard implements CanActivate { + constructor(private readonly subpoenaService: SubpoenaService) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest() + + const subpoenaId = request.params.subpoenaId + + if (!subpoenaId) { + throw new BadRequestException('Missing subpoena id') + } + + request.subpoena = await this.subpoenaService.findBySubpoenaId(subpoenaId) + + return true + } +} diff --git a/apps/judicial-system/backend/src/app/modules/subpoena/internalSubpoena.controller.ts b/apps/judicial-system/backend/src/app/modules/subpoena/internalSubpoena.controller.ts new file mode 100644 index 000000000000..a0f909375a80 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/subpoena/internalSubpoena.controller.ts @@ -0,0 +1,110 @@ +import { Base64 } from 'js-base64' + +import { + Body, + Controller, + Get, + Inject, + Param, + Patch, + Post, + UseGuards, +} from '@nestjs/common' +import { ApiOkResponse, ApiTags } from '@nestjs/swagger' + +import type { Logger } from '@island.is/logging' +import { LOGGER_PROVIDER } from '@island.is/logging' + +import { TokenGuard } from '@island.is/judicial-system/auth' +import { + messageEndpoint, + MessageType, +} from '@island.is/judicial-system/message' +import { indictmentCases } from '@island.is/judicial-system/types' + +import { + CaseExistsGuard, + CaseTypeGuard, + CurrentCase, + PdfService, +} from '../case' +import { Case } from '../case/models/case.model' +import { CurrentDefendant } from '../defendant/guards/defendant.decorator' +import { DefendantExistsGuard } from '../defendant/guards/defendantExists.guard' +import { Defendant } from '../defendant/models/defendant.model' +import { DeliverDto } from './dto/deliver.dto' +import { UpdateSubpoenaDto } from './dto/updateSubpoena.dto' +import { CurrentSubpoena } from './guards/subpoena.decorator' +import { SubpoenaExistsGuard } from './guards/subpoenaExists.guard' +import { DeliverResponse } from './models/deliver.response' +import { Subpoena } from './models/subpoena.model' +import { SubpoenaService } from './subpoena.service' + +@Controller('api/internal/') +@ApiTags('internal subpoenas') +@UseGuards(TokenGuard) +export class InternalSubpoenaController { + constructor( + private readonly subpoenaService: SubpoenaService, + private readonly pdfService: PdfService, + @Inject(LOGGER_PROVIDER) private readonly logger: Logger, + ) {} + + @UseGuards(SubpoenaExistsGuard) + @Get('subpoena/:subpoenaId') + async getSubpoena( + @Param('subpoenaId') subpoenaId: string, + @CurrentSubpoena() subpoena: Subpoena, + ): Promise { + this.logger.debug(`Getting subpoena by subpoena id ${subpoenaId}`) + + return subpoena + } + + @UseGuards(SubpoenaExistsGuard) + @Patch('subpoena/:subpoenaId') + async updateSubpoena( + @Param('subpoenaId') subpoenaId: string, + @CurrentSubpoena() subpoena: Subpoena, + @Body() update: UpdateSubpoenaDto, + ): Promise { + this.logger.debug(`Updating subpoena by subpoena id ${subpoenaId}`) + + return this.subpoenaService.update(subpoena, update) + } + + @UseGuards( + CaseExistsGuard, + new CaseTypeGuard(indictmentCases), + DefendantExistsGuard, + ) + @Post( + `case/:caseId/${ + messageEndpoint[MessageType.DELIVERY_TO_POLICE_SUBPOENA] + }/:defendantId`, + ) + @ApiOkResponse({ + type: DeliverResponse, + description: 'Delivers a subpoena to police', + }) + async deliverSubpoenaToPolice( + @Param('caseId') caseId: string, + @Param('defendantId') defendantId: string, + @CurrentCase() theCase: Case, + @CurrentDefendant() defendant: Defendant, + @Body() deliverDto: DeliverDto, + ): Promise { + this.logger.debug( + `Delivering subpoena ${caseId} to police for defendant ${defendantId}`, + ) + + const pdf = await this.pdfService.getSubpoenaPdf(theCase, defendant) + + return await this.subpoenaService.deliverSubpoenaToPolice( + theCase, + defendant, + Base64.btoa(pdf.toString('binary')), + deliverDto.user, + ) + } +} diff --git a/apps/judicial-system/backend/src/app/modules/subpoena/models/deliver.response.ts b/apps/judicial-system/backend/src/app/modules/subpoena/models/deliver.response.ts new file mode 100644 index 000000000000..93df1000e29f --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/subpoena/models/deliver.response.ts @@ -0,0 +1,6 @@ +import { ApiProperty } from '@nestjs/swagger' + +export class DeliverResponse { + @ApiProperty({ type: Boolean }) + delivered!: boolean +} diff --git a/apps/judicial-system/backend/src/app/modules/subpoena/models/subpoena.model.ts b/apps/judicial-system/backend/src/app/modules/subpoena/models/subpoena.model.ts new file mode 100644 index 000000000000..5a540fd3af27 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/subpoena/models/subpoena.model.ts @@ -0,0 +1,71 @@ +import { + BelongsTo, + Column, + CreatedAt, + DataType, + ForeignKey, + Model, + Table, + UpdatedAt, +} from 'sequelize-typescript' + +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger' + +import { Case } from '../../case/models/case.model' +import { Defendant } from '../../defendant/models/defendant.model' + +@Table({ + tableName: 'subpoena', + timestamps: true, +}) +export class Subpoena extends Model { + @Column({ + type: DataType.UUID, + primaryKey: true, + allowNull: false, + defaultValue: DataType.UUIDV4, + }) + @ApiProperty({ type: String }) + id!: string + + @CreatedAt + @ApiProperty({ type: Date }) + created!: Date + + @UpdatedAt + @ApiProperty({ type: Date }) + modified!: Date + + @ApiPropertyOptional({ type: String }) + @Column({ type: DataType.STRING, allowNull: true }) + subpoenaId?: string + + @ForeignKey(() => Defendant) + @Column({ type: DataType.UUID, allowNull: false }) + defendantId!: string + + @BelongsTo(() => Defendant, 'defendantId') + @ApiProperty({ type: Defendant }) + defendant?: Defendant + + @ForeignKey(() => Case) + @Column({ type: DataType.UUID, allowNull: true }) + @ApiProperty({ type: String }) + caseId?: string + + @BelongsTo(() => Case, 'caseId') + @ApiPropertyOptional({ type: Case }) + case?: Case + + @Column({ type: DataType.BOOLEAN, allowNull: true }) + @ApiPropertyOptional({ type: Boolean }) + acknowledged?: string + + @Column({ type: DataType.STRING, allowNull: true }) + @ApiPropertyOptional({ type: String }) + registeredBy?: string + + @Column({ type: DataType.TEXT, allowNull: true }) + @ApiPropertyOptional({ type: String }) + comment?: string +} diff --git a/apps/judicial-system/backend/src/app/modules/subpoena/subpoena.module.ts b/apps/judicial-system/backend/src/app/modules/subpoena/subpoena.module.ts new file mode 100644 index 000000000000..1f40a7844f60 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/subpoena/subpoena.module.ts @@ -0,0 +1,22 @@ +import { forwardRef, Module } from '@nestjs/common' +import { SequelizeModule } from '@nestjs/sequelize' + +import { CaseModule } from '../case/case.module' +import { DefendantModule } from '../defendant/defendant.module' +import { Defendant } from '../defendant/models/defendant.model' +import { PoliceModule } from '../police/police.module' +import { Subpoena } from './models/subpoena.model' +import { InternalSubpoenaController } from './internalSubpoena.controller' +import { SubpoenaService } from './subpoena.service' + +@Module({ + imports: [ + forwardRef(() => CaseModule), + forwardRef(() => DefendantModule), + forwardRef(() => PoliceModule), + SequelizeModule.forFeature([Subpoena, Defendant]), + ], + controllers: [InternalSubpoenaController], + providers: [SubpoenaService], +}) +export class SubpoenaModule {} diff --git a/apps/judicial-system/backend/src/app/modules/subpoena/subpoena.service.ts b/apps/judicial-system/backend/src/app/modules/subpoena/subpoena.service.ts new file mode 100644 index 000000000000..62d0355fd57c --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/subpoena/subpoena.service.ts @@ -0,0 +1,140 @@ +import { Includeable, Sequelize } from 'sequelize' +import { Transaction } from 'sequelize/types' + +import { forwardRef, Inject, Injectable } from '@nestjs/common' +import { InjectConnection, InjectModel } from '@nestjs/sequelize' + +import type { Logger } from '@island.is/logging' +import { LOGGER_PROVIDER } from '@island.is/logging' + +import type { User } from '@island.is/judicial-system/types' + +import { Case } from '../case/models/case.model' +import { Defendant } from '../defendant/models/defendant.model' +import { PoliceService } from '../police' +import { UpdateSubpoenaDto } from './dto/updateSubpoena.dto' +import { DeliverResponse } from './models/deliver.response' +import { Subpoena } from './models/subpoena.model' + +export const include: Includeable[] = [ + { model: Case, as: 'case' }, + { model: Defendant, as: 'defendant' }, +] +@Injectable() +export class SubpoenaService { + constructor( + @InjectConnection() private readonly sequelize: Sequelize, + @InjectModel(Subpoena) private readonly subpoenaModel: typeof Subpoena, + @InjectModel(Defendant) private readonly defendantModel: typeof Defendant, + @Inject(forwardRef(() => PoliceService)) + private readonly policeService: PoliceService, + @Inject(LOGGER_PROVIDER) private readonly logger: Logger, + ) {} + + async createSubpoena(defendant: Defendant): Promise { + return await this.subpoenaModel.create({ + defendantId: defendant.id, + caseId: defendant.caseId, + }) + } + + async update( + subpoena: Subpoena, + update: UpdateSubpoenaDto, + transaction?: Transaction, + ): Promise { + const { + defenderChoice, + defenderNationalId, + defenderEmail, + defenderPhoneNumber, + defenderName, + } = update + + const [numberOfAffectedRows] = await this.subpoenaModel.update(update, { + where: { subpoenaId: subpoena.subpoenaId }, + returning: true, + transaction, + }) + let defenderAffectedRows = 0 + + if (defenderChoice || defenderNationalId) { + const defendantUpdate: Partial = { + defenderChoice, + defenderNationalId, + defenderName, + defenderEmail, + defenderPhoneNumber, + } + + const [defenderUpdateAffectedRows] = await this.defendantModel.update( + defendantUpdate, + { + where: { id: subpoena.defendantId }, + transaction, + }, + ) + + defenderAffectedRows = defenderUpdateAffectedRows + } + + if (numberOfAffectedRows < 1 && defenderAffectedRows < 1) { + this.logger.error( + `Unexpected number of rows ${numberOfAffectedRows} affected when updating subpoena`, + ) + } + + const updatedSubpoena = await this.findBySubpoenaId(subpoena.subpoenaId) + return updatedSubpoena + } + + async findBySubpoenaId(subpoenaId?: string): Promise { + if (!subpoenaId) { + throw new Error('Missing subpoena id') + } + + const subpoena = await this.subpoenaModel.findOne({ + include, + where: { subpoenaId }, + }) + + if (!subpoena) { + throw new Error(`Subpoena with id ${subpoenaId} not found`) + } + + return subpoena + } + + async deliverSubpoenaToPolice( + theCase: Case, + defendant: Defendant, + subpoenaFile: string, + user: User, + ): Promise { + try { + const subpoena = await this.createSubpoena(defendant) + + const createdSubpoena = await this.policeService.createSubpoena( + theCase, + defendant, + subpoenaFile, + user, + ) + + if (!createdSubpoena) { + this.logger.error('Failed to create subpoena file for police') + return { delivered: false } + } + + await this.subpoenaModel.update( + { subpoenaId: createdSubpoena.subpoenaId }, + { where: { id: subpoena.id } }, + ) + + return { delivered: true } + } catch (error) { + this.logger.error('Error delivering subpoena to police', error) + return { delivered: false } + } + } +} diff --git a/apps/judicial-system/xrd-api/src/app/app.controller.ts b/apps/judicial-system/xrd-api/src/app/app.controller.ts index 1d6669745a8e..d5e3a6fd9de8 100644 --- a/apps/judicial-system/xrd-api/src/app/app.controller.ts +++ b/apps/judicial-system/xrd-api/src/app/app.controller.ts @@ -3,6 +3,9 @@ import { Controller, Inject, InternalServerErrorException, + Param, + ParseUUIDPipe, + Patch, Post, UseInterceptors, } from '@nestjs/common' @@ -14,6 +17,8 @@ import { LOGGER_PROVIDER } from '@island.is/logging' import { LawyersService, LawyerType } from '@island.is/judicial-system/lawyers' +import { UpdateSubpoenaDto } from './dto/subpoena.dto' +import { SubpoenaResponse } from './models/subpoena.response' import { CreateCaseDto } from './app.dto' import { EventInterceptor } from './app.interceptor' import { Case, Defender } from './app.model' @@ -59,4 +64,15 @@ export class AppController { throw new InternalServerErrorException('Failed to retrieve lawyers') } } + + @Patch('subpoena/:subpoenaId') + @ApiResponse({ status: 500, description: 'Failed to update subpoena' }) + async updateSubpoena( + @Param('subpoenaId', new ParseUUIDPipe()) subpoenaId: string, + @Body() updateSubpoena: UpdateSubpoenaDto, + ): Promise { + this.logger.debug(`Updating subpoena for ${subpoenaId}`) + + return this.appService.updateSubpoena(subpoenaId, updateSubpoena) + } } diff --git a/apps/judicial-system/xrd-api/src/app/app.service.ts b/apps/judicial-system/xrd-api/src/app/app.service.ts index b80626d915bd..77da007af1cf 100644 --- a/apps/judicial-system/xrd-api/src/app/app.service.ts +++ b/apps/judicial-system/xrd-api/src/app/app.service.ts @@ -15,7 +15,11 @@ import { AuditedAction, AuditTrailService, } from '@island.is/judicial-system/audit-trail' +import { LawyersService } from '@island.is/judicial-system/lawyers' +import { DefenderChoice } from '@island.is/judicial-system/types' +import { UpdateSubpoenaDto } from './dto/subpoena.dto' +import { SubpoenaResponse } from './models/subpoena.response' import appModuleConfig from './app.config' import { CreateCaseDto } from './app.dto' import { Case } from './app.model' @@ -26,8 +30,17 @@ export class AppService { @Inject(appModuleConfig.KEY) private readonly config: ConfigType, private readonly auditTrailService: AuditTrailService, + private readonly lawyersService: LawyersService, @Inject(LOGGER_PROVIDER) private readonly logger: Logger, ) {} + async create(caseToCreate: CreateCaseDto): Promise { + return this.auditTrailService.audit( + 'xrd-api', + AuditedAction.CREATE_CASE, + this.createCase(caseToCreate), + (theCase) => theCase.id, + ) + } private async createCase(caseToCreate: CreateCaseDto): Promise { return fetch(`${this.config.backend.url}/api/internal/case/`, { @@ -67,12 +80,90 @@ export class AppService { }) } - async create(caseToCreate: CreateCaseDto): Promise { - return this.auditTrailService.audit( - 'xrd-api', - AuditedAction.CREATE_CASE, - this.createCase(caseToCreate), - (theCase) => theCase.id, + async updateSubpoena( + subpoenaId: string, + updateSubpoena: UpdateSubpoenaDto, + ): Promise { + return await this.auditTrailService.audit( + 'digital-mailbox-api', + AuditedAction.UPDATE_SUBPOENA, + this.updateSubpoenaInfo(subpoenaId, updateSubpoena), + subpoenaId, ) } + + private async updateSubpoenaInfo( + subpoenaId: string, + updateSubpoena: UpdateSubpoenaDto, + ): Promise { + let update = { ...updateSubpoena } + + if ( + update.defenderChoice === DefenderChoice.CHOOSE && + !update.defenderNationalId + ) { + throw new BadRequestException( + 'Defender national id is required for choice', + ) + } + + if (update.defenderNationalId) { + try { + const chosenLawyer = await this.lawyersService.getLawyer( + update.defenderNationalId, + ) + update = { + ...update, + ...{ + defenderName: chosenLawyer.Name, + defenderEmail: chosenLawyer.Email, + defenderPhoneNumber: chosenLawyer.Phone, + }, + } + } catch (reason) { + throw new BadRequestException('Lawyer not found') + } + } + + try { + const res = await fetch( + `${this.config.backend.url}/api/internal/subpoena/${subpoenaId}`, + { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + authorization: `Bearer ${this.config.backend.accessToken}`, + }, + body: JSON.stringify(update), + }, + ) + + const response = await res.json() + + if (res.ok) { + return { + subpoenaComment: response.comment, + defenderInfo: { + defenderChoice: response.defendant.defenderChoice, + defenderName: response.defendant.defenderName, + }, + } as SubpoenaResponse + } + + if (res.status < 500) { + throw new BadRequestException(response?.detail) + } + + throw response + } catch (reason) { + if (reason instanceof BadRequestException) { + throw reason + } + + throw new BadGatewayException({ + ...reason, + message: 'Failed to update subpoena', + }) + } + } } diff --git a/apps/judicial-system/xrd-api/src/app/dto/subpoena.dto.ts b/apps/judicial-system/xrd-api/src/app/dto/subpoena.dto.ts new file mode 100644 index 000000000000..7ac2807b0467 --- /dev/null +++ b/apps/judicial-system/xrd-api/src/app/dto/subpoena.dto.ts @@ -0,0 +1,32 @@ +import { IsBoolean, IsEnum, IsOptional, IsString } from 'class-validator' + +import { ApiProperty } from '@nestjs/swagger' + +import { DefenderChoice } from '@island.is/judicial-system/types' + +export class UpdateSubpoenaDto { + @IsOptional() + @IsBoolean() + @ApiProperty({ type: Boolean, required: false }) + acknowledged?: boolean + + @IsOptional() + @IsString() + @ApiProperty({ type: String, required: false }) + comment?: string + + @IsOptional() + @IsString() + @ApiProperty({ type: String, required: false }) + registeredBy?: string + + @IsOptional() + @IsEnum(DefenderChoice) + @ApiProperty({ enum: DefenderChoice, required: false }) + defenderChoice?: DefenderChoice + + @IsOptional() + @IsString() + @ApiProperty({ type: String, required: false }) + defenderNationalId?: string +} diff --git a/apps/judicial-system/xrd-api/src/app/models/subpoena.response.ts b/apps/judicial-system/xrd-api/src/app/models/subpoena.response.ts new file mode 100644 index 000000000000..e3aacfd7b51a --- /dev/null +++ b/apps/judicial-system/xrd-api/src/app/models/subpoena.response.ts @@ -0,0 +1,22 @@ +import { IsEnum } from 'class-validator' + +import { ApiProperty } from '@nestjs/swagger' + +import { DefenderChoice } from '@island.is/judicial-system/types' + +export class SubpoenaResponse { + @ApiProperty({ type: () => Boolean }) + acknowledged?: boolean + + @ApiProperty({ type: () => DefenderInfo }) + defenderInfo?: DefenderInfo +} + +class DefenderInfo { + @IsEnum(DefenderChoice) + @ApiProperty({ enum: DefenderChoice }) + defenderChoice!: DefenderChoice + + @ApiProperty({ type: String }) + defenderName?: string +} diff --git a/libs/judicial-system/formatters/src/lib/formatters.ts b/libs/judicial-system/formatters/src/lib/formatters.ts index 1c974929a9d4..b87283fc7de5 100644 --- a/libs/judicial-system/formatters/src/lib/formatters.ts +++ b/libs/judicial-system/formatters/src/lib/formatters.ts @@ -131,7 +131,7 @@ export const getHumanReadableCaseIndictmentRulingDecision = ( type CaseTypes = { [c in CaseType]: string } const caseTypes: CaseTypes = { - // Indicitment cases + // Indictment cases INDICTMENT: 'ákæra', // Restriction cases CUSTODY: 'gæsluvarðhald', diff --git a/libs/judicial-system/message/src/lib/message.ts b/libs/judicial-system/message/src/lib/message.ts index 4d432bbfc7d5..99bfc42f74b0 100644 --- a/libs/judicial-system/message/src/lib/message.ts +++ b/libs/judicial-system/message/src/lib/message.ts @@ -22,6 +22,7 @@ export enum MessageType { DELIVERY_TO_POLICE_INDICTMENT_CASE = 'DELIVERY_TO_POLICE_INDICTMENT_CASE', DELIVERY_TO_POLICE_INDICTMENT = 'DELIVERY_TO_POLICE_INDICTMENT', DELIVERY_TO_POLICE_CASE_FILES_RECORD = 'DELIVERY_TO_POLICE_CASE_FILES_RECORD', + DELIVERY_TO_POLICE_SUBPOENA = 'DELIVERY_TO_POLICE_SUBPOENA', DELIVERY_TO_POLICE_SIGNED_RULING = 'DELIVERY_TO_POLICE_SIGNED_RULING', DELIVERY_TO_POLICE_APPEAL = 'DELIVERY_TO_POLICE_APPEAL', NOTIFICATION = 'NOTIFICATION', @@ -54,6 +55,7 @@ export const messageEndpoint: { [key in MessageType]: string } = { DELIVERY_TO_POLICE_INDICTMENT_CASE: 'deliverIndictmentCaseToPolice', DELIVERY_TO_POLICE_INDICTMENT: 'deliverIndictmentToPolice', DELIVERY_TO_POLICE_CASE_FILES_RECORD: 'deliverCaseFilesRecordToPolice', + DELIVERY_TO_POLICE_SUBPOENA: 'deliverSubpoenaToPolice', DELIVERY_TO_POLICE_SIGNED_RULING: 'deliverSignedRulingToPolice', DELIVERY_TO_POLICE_APPEAL: 'deliverAppealToPolice', NOTIFICATION: 'notification',